1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-12 21:43:41 +02:00

Compare commits

..

601 Commits

Author SHA1 Message Date
6db909a3e7 update migration 2019-12-25 20:27:30 +04:00
642ae161c0 prep actix-web release 2019-12-25 20:21:00 +04:00
7b3c99b933 prep actix-framed release 2019-12-25 20:17:22 +04:00
f86ce0390e allow to specify multi pattern for resources 2019-12-25 20:14:44 +04:00
7882f545e5 Allow to gracefully stop test server via TestServer::stop() 2019-12-25 12:10:48 +04:00
1c75e6876b update migration 2019-12-22 17:16:07 +04:00
6a0cd2dced Rename HttpServer::start() to HttpServer::run() 2019-12-22 17:12:22 +04:00
c7f3915779 update actix-service dep 2019-12-22 16:39:25 +04:00
f45db1f909 Enable GitHub Actions and fix file URL behavior (#1232)
* Use GitHub Actions

* Fix unused imports on Windows

* Fix test for Windows

* Stop to run CI for i686-pc-windows-msvc for now

* Use `/` instead of `\` on Windows

* Add entry to changelog

* Prepare actix-files release
2019-12-22 16:43:41 +09:00
3751a4018e fixed test::init_service api docs (missing await) (#1230) 2019-12-21 08:47:18 +06:00
0cb1b0642f add test server data test 2019-12-20 23:18:59 +06:00
48476362a3 update changes 2019-12-20 17:59:34 +06:00
2b4256baab add links to configs 2019-12-20 17:49:05 +06:00
e5a50f423d Make web::Data deref to Arc<T> #1214 2019-12-20 17:45:35 +06:00
8b8a9a995d bump ver 2019-12-20 17:36:48 +06:00
74fa4060c2 fix awc tests 2019-12-20 17:27:32 +06:00
c877840c07 rename App::register_data to App::app_data and HttpRequest::app_data returns Option<&T> instead of Option<&Data<T>> 2019-12-20 17:13:09 +06:00
20248daeda Allow to set peer_addr for TestRequest #1074 2019-12-20 16:11:51 +06:00
a08d8dab70 AppConfig::secure() is always false. #1202 2019-12-20 16:04:51 +06:00
fbbb4a86e9 feat: add access to the session also from immutable references (#1225) 2019-12-20 13:59:07 +06:00
1d12ba9d5f Replace brotli with brotli2 #1224 2019-12-20 13:50:07 +06:00
8c54054844 Use .advance() intead of .split_to() 2019-12-19 09:56:14 +06:00
1732ae8c79 fix Bodyencoding trait usage 2019-12-18 09:30:14 +06:00
3b860ebdc7 Fix poll_ready call for WebSockets upgrade (#1219)
* Fix poll_ready call for WebSockets upgrade

* Poll upgrade service from H1ServiceHandler too
2019-12-17 13:34:25 +06:00
29ac6463e1 Merge branch 'master' of github.com:actix/actix-web 2019-12-16 17:22:49 +06:00
01613f334b Move BodyEncoding to dev module #1220 2019-12-16 17:22:26 +06:00
30dcaf9da0 fix deprecated Error::description (#1218) 2019-12-16 07:43:19 +06:00
b0aa9395da prep actix-web alpha.6 release 2019-12-15 22:51:14 +06:00
a153374b61 migrate actix-web-actors 2019-12-15 22:45:38 +06:00
a791aab418 prep awc release 2019-12-15 13:36:05 +06:00
cb705317b8 compile with default-features off 2019-12-15 13:28:54 +06:00
e8e0f98f96 fix docs.rs features list 2019-12-13 12:41:48 +06:00
c878f66d05 fix docs.rs features list 2019-12-13 12:40:22 +06:00
fac6dec3c9 update deps 2019-12-13 12:36:15 +06:00
232f71b3b5 update changes 2019-12-13 12:18:30 +06:00
8881c13e60 update changes 2019-12-13 12:16:43 +06:00
d006a7b31f update changes 2019-12-13 12:10:45 +06:00
3d64d565d9 fix warnings 2019-12-13 11:46:02 +06:00
c1deaaeb2f cleanup imports 2019-12-13 11:24:57 +06:00
b81417c2fa fix warnings 2019-12-13 10:59:02 +06:00
4937c9f9c2 refactor http-test server 2019-12-12 23:08:38 +06:00
db1d6b7963 refactor test server impl 2019-12-12 22:28:47 +06:00
fa07415721 Replace flate2-xxx features with compress 2019-12-12 15:08:08 +06:00
b4b3350b3e Add websockets continuation frame support 2019-12-12 14:06:54 +06:00
4a1695f719 fixes missing import in example (#1210) 2019-12-12 07:06:22 +06:00
1b8d747937 Fix extra line feed (#1209) 2019-12-12 07:05:39 +06:00
a43a005f59 Log path if it is not a directory (#1208) 2019-12-12 07:04:53 +06:00
a612b74aeb actix-multipart: Fix multipart boundary reading (#1205)
* actix-multipart: Fix multipart boundary reading

If we're not ready to read the first line after the multipart field
(which should be a "\r\n" line) then return Pending instead of Ready(None)
so that we will get called again to read that line.

Without this I was getting MultipartError::Boundary from read_boundary()
because it got the "\r\n" line instead of the boundary.

Also tweaks the test_stream test to test partial reads.

This is a forward port of #1189 from 1.0

* actix-multipart: Update changes for boundary fix
2019-12-12 07:03:44 +06:00
131c897099 upgrade to actix-net release 2019-12-11 19:20:20 +06:00
ef3a33b9d6 use std mutext instead of parking_lot 2019-12-10 09:00:51 +06:00
5132257b0d Fix buffer remaining capacity calcualtion 2019-12-09 21:55:22 +06:00
0c1f5f9edc Check Upgrade service readiness before calling it 2019-12-09 17:40:15 +06:00
e4382e4fc1 Fix broken docs (#1204)
Fixed un escaped brackets in lib.rs, and reflowed links to ConnectionInfo in app, config, and server.rs
2019-12-09 10:02:43 +06:00
a3ce371312 ws ping and pong uses bytes #1049 2019-12-09 07:01:22 +06:00
42258ee289 deps 2019-12-08 20:22:39 +06:00
b92eafb839 prepare actix-http release 2019-12-08 20:15:51 +06:00
3b2e78db47 add link to chat 2019-12-08 19:27:06 +06:00
63da1a5560 Merge branch 'master' of github.com:actix/actix-web 2019-12-08 19:26:12 +06:00
1f3ffe38e8 update actix-service dep 2019-12-08 19:25:24 +06:00
c23b6b3879 Merge pull request #1192 from krircc/master
Add rich project metadata
2019-12-08 16:03:39 +08:00
909c7c8b5b Merge branch 'master' into master 2019-12-08 16:26:35 +09:00
4a8a9ef405 update tests and clippy warnings 2019-12-08 12:31:16 +06:00
6c9f9fff73 clippy warnings 2019-12-08 00:46:51 +06:00
8df33f7a81 remove HttpServer::run() as it is not useful with async/await 2019-12-08 00:06:04 +06:00
7ec5ca88a1 update changes 2019-12-07 22:01:55 +06:00
e5f3d88a4e Switch brotli compressor to rust. (#1197)
* Switch to a rustified version of brotli.

* Some memory optimizations.

* Make brotli not optional anymore.
2019-12-07 21:55:41 +06:00
0ba125444a Add impl ResponseBuilder for Error 2019-12-07 21:41:34 +06:00
6c226e47bd prepare actix-web-actors release 2019-12-07 20:10:36 +06:00
8c3f58db9d Allow comma-separated websocket subprotocols without spaces (#1172)
* Allow comma-separated websocket subprotocols without spaces

* [CHANGES] Added an entry to CHANGES.md
2019-12-07 20:08:06 +06:00
4921243add Fix rustls build. (#1195) 2019-12-07 16:14:09 +06:00
91b3fcf85c Fix dependency features. (#1196) 2019-12-07 16:13:26 +06:00
1729a52f8b prepare alpha.3 release 2019-12-07 13:00:03 +06:00
ed2f3fe80d use actix-net alpha.3 release 2019-12-07 12:28:26 +06:00
f2ba389496 Merge branch 'master' into master 2019-12-06 16:57:42 +09:00
439f02b6b1 Update README.md 2019-12-06 14:59:11 +08:00
e32da08a26 Update README.md 2019-12-06 14:34:14 +08:00
82110e0927 Update README.md 2019-12-06 14:29:10 +08:00
7b3354a9ad Update README.md 2019-12-06 14:26:23 +08:00
5243e8baca Update README.md 2019-12-06 14:23:28 +08:00
98903028c7 Update README.md 2019-12-06 14:22:29 +08:00
7dd676439c update changes for actix-session 2019-12-06 11:24:25 +06:00
fbead137f0 feat: add access to UserSession from RequestHead (#1164)
* feat: add access to UserSession from RequestHead

* add test case for session from RequestHead and changes entry for the new feature
2019-12-06 11:21:43 +06:00
205a964d8f upgrade to tokio 0.2 2019-12-05 23:35:43 +06:00
b45c6cd66b replace hashbrown with std hashmap 2019-12-04 18:33:43 +06:00
0015a204aa update version 2019-12-03 19:03:53 +06:00
c7ed6d3428 update version 2019-12-03 16:35:31 +06:00
cf30eafb49 update md 2019-12-03 00:49:12 +06:00
14075ebf7f use released versions of actix-net 2019-12-02 23:33:39 +06:00
068f047dd5 update service factory config 2019-12-02 21:37:13 +06:00
f4c01384ec update to latest actix-net 2019-12-02 17:33:11 +06:00
b7d44d6c4c Merge pull request #1 from actix/master
git pull
2019-12-01 16:56:42 +08:00
33574403b5 Remove rustls from package.metadata.docs.rs (#1182) 2019-11-28 06:25:21 +06:00
dcc6efa3e6 Merge branch 'master' of github.com:actix/actix-web 2019-11-27 21:08:13 +06:00
56b9f11c98 disable rustls 2019-11-27 21:07:49 +06:00
f43a706364 Set name for each generated resource 2019-11-26 19:25:28 +06:00
f2b3dc5625 update examples 2019-11-26 17:16:33 +06:00
f73f97353b refactor ResponseError trait 2019-11-26 16:07:39 +06:00
4dc31aac93 use actix_rt::test for test setup 2019-11-26 11:25:50 +06:00
c1c44a7dd6 upgrade derive_more 2019-11-25 17:59:14 +06:00
c5907747ad Remove implementation of Responder for (). Fixes #1108.
Rationale:

- In Rust, one can omit a semicolon after a function's final expression to make
  its value the function's return value. It's common for people to include a
  semicolon after the last expression by mistake - common enough that the Rust
  compiler suggests removing the semicolon when there's a type mismatch between
  the function's signature and body. By implementing Responder for (), Actix makes
  this common mistake a silent error in handler functions.

- Functions returning an empty body should return HTTP status 204 ("No Content"),
  so the current Responder impl for (), which returns status 200 ("OK"), is not
  really what one wants anyway.

- It's not much of a burden to ask handlers to explicitly return
  `HttpResponse::Ok()` if that is what they want; all the examples in the
  documentation do this already.
2019-11-23 21:10:02 +06:00
525c22de15 fix typos from updating to futures 0.3 2019-11-22 13:25:55 +06:00
57981ca04a update tests to async handlers 2019-11-22 11:49:35 +06:00
e668acc596 update travis config 2019-11-22 10:13:32 +06:00
512dd2be63 disable rustls support 2019-11-22 07:01:05 +06:00
8683ba8bb0 rename .to_async() to .to() 2019-11-21 21:36:35 +06:00
0b9e3d381b add test with custom connector 2019-11-21 17:36:18 +06:00
1f0577f8d5 cleanup api doc examples 2019-11-21 16:02:17 +06:00
53c5151692 use response instead of result for asyn c handlers 2019-11-21 16:02:17 +06:00
55698f2524 migrade rest of middlewares 2019-11-21 16:02:17 +06:00
471f82f0e0 migrate actix-multipart 2019-11-21 16:02:17 +06:00
60ada97b3d migrate actix-session 2019-11-21 16:02:17 +06:00
0de101bc4d update actix-web-codegen tests 2019-11-21 16:02:17 +06:00
95e2a0ef2e migrate actix-framed 2019-11-21 16:02:17 +06:00
69cadcdedb migrate actix-files 2019-11-21 16:02:17 +06:00
6ac4ac66b9 migrate actix-cors 2019-11-21 16:02:17 +06:00
3646725cf6 migrate actix-identity 2019-11-21 16:02:17 +06:00
ff62facc0d disable unmigrated crates 2019-11-21 16:02:17 +06:00
b510527a9f update awc tests 2019-11-21 16:02:17 +06:00
3127dd4db6 migrate actix-web to std::future 2019-11-21 16:02:17 +06:00
d081e57316 fix h2 client send body 2019-11-21 16:02:17 +06:00
1ffa7d18d3 drop unpin constraint 2019-11-21 16:02:17 +06:00
687884fb94 update test-server tests 2019-11-21 16:02:17 +06:00
5ab29b2e62 migrate awc and test-server to std::future 2019-11-21 16:02:17 +06:00
a6a2d2f444 update ssl impls 2019-11-21 16:02:17 +06:00
9e95efcc16 migrate client to std::future 2019-11-21 16:02:17 +06:00
8cba1170e6 make actix-http compile with std::future 2019-11-21 16:02:17 +06:00
5cb2d500d1 update actix-web-actors 2019-11-14 08:58:24 +06:00
0212c618c6 prepare actix-web release 2019-11-14 08:55:37 +06:00
88110ed268 Add security note to ConnectionInfo::remote() (#1158) 2019-11-14 08:32:47 +06:00
fba02fdd8c prep awc release 2019-11-06 11:33:25 -08:00
b2934ad8d2 prep actix-file release 2019-11-06 11:25:26 -08:00
f7f410d033 fix test order dep 2019-11-06 11:20:47 -08:00
885ff7396e prepare actox-http release 2019-11-06 10:35:13 -08:00
61b38e8d0d Increase timeouts in test-server (#1153) 2019-11-06 06:09:22 -08:00
edcde67076 Fix escaping/encoding problems in Content-Disposition header (#1151)
* Fix filename encoding in Content-Disposition of acitx_files::NamedFile

* Add more comments on how to use Content-Disposition header properly & Fix some trivial problems

* Improve Content-Disposition filename(*) parameters of actix_files::NamedFile

* Tweak Content-Disposition parse to accept empty param value in quoted-string

* Fix typos in comments in .../content_disposition.rs (pointed out by @JohnTitor)

* Update CHANGES.md

* Update CHANGES.md again
2019-11-06 06:08:37 -08:00
f0612f7570 awc: Add support for setting query from Serialize type for client request (#1130)
Signed-off-by: Jonathas-Conceicao <jadoliveira@inf.ufpel.edu.br>
2019-10-26 08:27:14 +03:00
ace98e3a1e support Host guards when Host header is unset (#1129) 2019-10-15 05:05:54 +06:00
1ca9d87f0a prep actix-web-codegen release 2019-10-14 21:35:53 +06:00
967f965405 Update syn & quote to 1.0 (#1133)
* chore(actix-web-codegen): Upgrade syn and quote to 1.0

* feat(actix-web-codegen): Generate better error message

* doc(actix-web-codegen): Update CHANGES.md

* fix: Build with stable rust
2019-10-14 21:34:17 +06:00
062e51e8ce prep actix-file release 2019-10-14 21:26:26 +06:00
a48e616def feat(files): add possibility to redirect to slash-ended path (#1134)
When accessing to a folder without a final slash, the index file will be loaded ok, but if it has
references (like a css or an image in an html file) these resources won't be loaded correctly if
they are using relative paths. In order to solve this, this PR adds the possibility to detect
folders without a final slash and make a 302 redirect to mitigate this issue. The behavior is off by
default. We're adding a new method called `redirect_to_slash_directory` which can be used to enable
this behavior.
2019-10-14 21:23:15 +06:00
effa96f5e4 Removed httpcode 'MovedPermanenty'. (#1128) 2019-10-12 06:45:12 +06:00
cc0b4be5b7 Fix typo in response.rs body() comment (#1126)
Fixes https://github.com/actix/actix-web/issues/1125
2019-10-09 19:11:55 +06:00
a464ffc23d prepare actix-files release 2019-10-08 10:13:16 +06:00
4de2e8a898 [actix-files] Allow user defined guards for NamedFile (actix#1113) (#1115)
* [actix-files] remove request method checks from NamedFile

* [actix-files] added custom guard checks to FilesService

* [actix-files] modify method check tests (NamedFile -> Files)

* [actix-files] add test for custom guards in Files

* [actix-files] update changelog
2019-10-08 10:09:40 +06:00
0f09415469 Convert documentation examples to Rust 2018 edition (#1120)
* Convert types::query examples to rust-2018 edition

* Convert types::json examples to rust-2018 edition

* Convert types::path examples to rust-2018 edition

* Convert types::form examples to rust-2018 edition

* Convert rest of the examples to rust-2018 edition.
2019-10-07 11:29:11 +06:00
f089cf185b Let ResponseError render w/ 'text/plain; charset=utf-8' header (#1118) (#1119)
* Let ResponseError render w/ 'text/plain; charset=utf-8' header (#1118)

Trait ResponseError originally render Error messages with header
`text/plain` , which causes browsers (i.e. Firefox 70.0) with
Non-English locale unable to render UTF-8 responses with non-English
characters correctly. i.e. emoji.

This fix solved this problem by specifying the charset of `text/plain`
as utf-8, which is the default charset in rust.

Before actix-web consider to support other charsets, this hotfix is
 enough.

Test case:

fn test() -> Result<String, actix_web::Error> {
    Err(actix_web::error::ErrorForbidden("😋test"))
}

* Update actix-http/CHANGES.md for #1118
2019-10-07 10:56:24 +06:00
15d3c1ae81 Update docs of guard.rs (#1116)
* Update guard.rs
2019-10-07 12:05:17 +09:00
fba31d4e0a Expose ContentDisposition in actix-multipart to fix broken doc link (#1114)
* Expose ContentDisposition in actix-multipart to fix broken doc link

* Revert "Expose ContentDisposition in actix-multipart to fix broken doc link"

This reverts commit e90d71d16c.

* Unhide actix-http::header::common docs

These types are used in other exported documented interfaces and create
broken links if not documented.
See `actix_multipart::Field.content_disposition`
2019-10-02 09:48:25 +06:00
f81ae37677 Add From<Payload> for crate::dev::Payload (#1110)
* Add From<Payload> for crate::dev::Payload

* Make dev::Payload field of Payload public and add into_inner method

* Add changelog entry
2019-10-01 14:05:38 +06:00
5169d306ae update ConnectionInfo.remote() doc string 2019-09-27 07:03:12 +06:00
4f3e97fff8 prepare actix-web release 2019-09-25 15:39:09 +06:00
3ff01a9fc4 Add changelog entry for #1101 (#1102) 2019-09-25 15:35:28 +06:00
3d4e45a0e5 prepare release 2019-09-25 15:30:20 +06:00
c659c33919 Feature uds: Add listen_uds to ServerBuilder (#1085)
Allows using an existing Unix Listener instead of binding to a path.
Useful for when running as a daemon under systemd.

Change-Id: I54a0e78c321d8b7a9ded381083217af590e9a7fa
2019-09-25 15:16:51 +06:00
959f7754b2 Merge pull request #1101 from actix/add-awc-get-head-methods
Add remaining getter methods from private head field
2019-09-25 10:23:23 +02:00
23f04c4f38 Add remaining getter methods from private head field 2019-09-25 08:50:45 +02:00
d9af8f66ba Use actix-testing for testing utils 2019-09-25 10:28:41 +06:00
aa39b8ca6f Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() (#1096)
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()

* Update actix-http/CHANGES.md
2019-09-25 09:33:52 +06:00
58c7065f08 Implement register_data method on Resource and Scope. (#1094)
* Implement `register_data` method on `Resource` and `Scope`.

* Split Scope::register_data tests out from Scope::data tests.

* CHANGES.md: Mention {Scope,Resource}::register_data.
2019-09-18 06:36:39 +06:00
b3783b403e Merge branch 'master' of github.com:actix/actix-web 2019-09-17 21:46:45 +06:00
e4503046de Do not override current System 2019-09-17 21:45:06 +06:00
32a1c36597 Make UrlencodedError::Overflow more informative (#1089) 2019-09-17 06:58:04 +06:00
7c9f9afc46 Add ability to use Infallible as HttpResponse error type (#1093)
* Add `std::convert::Infallible` implementantion for `ResponseError`

* Add from `std::convert::Infallible` to `Error`

* Remove `ResponseError` implementantion for `Infallible`

* Remove useless docs

* Better comment

* Update changelog

* Update actix_http::changelog
2019-09-17 06:57:38 +06:00
c1f99e0775 Remove mem::uninitialized() (#1090) 2019-09-16 07:52:23 +09:00
a32573bb58 Allow to re-construct ServiceRequest from HttpRequest and Payload #1088 2019-09-13 11:56:24 +06:00
e35d930ef9 prepare releases 2019-09-12 21:58:08 +06:00
60b7aebd0a fmt & clippy 2019-09-12 21:52:46 +06:00
45d2fd4299 export frozen request related types; refactor code layout 2019-09-12 10:40:56 +06:00
71f8577713 prepare awc release 2019-09-11 20:13:28 +06:00
043f763c51 prepare actix-http release 2019-09-11 20:07:39 +06:00
8873e9b39e Added FrozenClientRequest for easier retrying HTTP calls (#1064)
* Initial commit

* Added extra_headers

* Added freeze() method to ClientRequest which produces a 'read-only' copy of a request suitable for retrying the send operation

* Additional methods for FrozenClientRequest

* Fix

* Increased crates versions

* Fixed a unit test. Added one more unit test.

* Added RequestHeaderWrapper

* Small fixes

* Renamed RequestHeadWrapper->RequestHeadType

* Updated CHANGES.md files

* Small fix

* Small changes

* Removed *_extra methods from Connection trait

* Added FrozenSendBuilder

* Added FrozenSendBuilder

* Minor fix

* Replaced impl Future with concrete Future implementation

* Small renaming

* Renamed Send->SendBody
2019-09-10 10:29:32 +06:00
5e8f1c338c fix h2 not using error response (#1080)
* fix h2 not using error response

* add fix change log

* fix h2 service error tests
2019-09-09 16:24:57 +06:00
1d96ae9bc3 actix-multipart: Correctly parse multipart body which does not end in CRLF (#1042)
* Correctly parse multipart body which does not end in CRLF

* Add in an eof guard for extra safety
2019-09-09 13:58:00 +06:00
8d61fe0925 Ensure that awc::ws::WebsocketsRequest sets the Host header (#1070)
* Ensure that awc::ws::WebsocketsRequest sets the Host header before connecting.

* Make sure to check if headers already have a HOST value before setting

* Update CHANGES.md to reflect WebSocket client update.
2019-09-09 12:27:13 +06:00
8a9fcddb3c Condition middleware (#1075)
* add condition middleware

* write tests

* update changes

* Update src/middleware/condition.rs

Co-Authored-By: Yuki Okushi <huyuumi.dev@gmail.com>

* Update src/middleware/condition.rs

Co-Authored-By: Yuki Okushi <huyuumi.dev@gmail.com>

* Update src/middleware/condition.rs

Co-Authored-By: Yuki Okushi <huyuumi.dev@gmail.com>

* Update src/middleware/condition.rs

Co-Authored-By: Yuki Okushi <huyuumi.dev@gmail.com>
2019-09-09 12:26:38 +06:00
c9400456f6 update actix-connect ver 2019-09-02 15:20:28 -07:00
63ddd30ee4 on_connect result isnt added to request extensions for http2 requests #1009 2019-09-01 13:15:02 +06:00
bae29897d6 prep actix-web release 2019-08-29 09:36:16 +06:00
616981ecf9 clear extensions before reclaiming HttpRequests in their pool (#1063)
Issue #1062
2019-08-29 09:35:05 +06:00
98bf8ab098 enable rust-tls feature for actix_web::client #1045 2019-08-28 21:40:24 +06:00
c193137905 actix_web::test::TestRequest::set_form (#1058) 2019-08-28 21:32:17 +06:00
a07cdd6533 Data::into_inner 2019-08-27 17:25:25 +01:00
61e492e7e3 Prepare actix-multipart 0.1.3 release 2019-08-18 10:39:22 +09:00
23d768a77b Add explicit dyns (#1041)
* Add explicit `dyn`s

* Remove unnecessary lines
2019-08-17 02:45:44 +09:00
87b7162473 chore(readme): fix copy paste error (#1040)
Fix actix-cors README
2019-08-16 09:21:30 +09:00
979c4d44f4 update awc dep 2019-08-13 12:41:26 -07:00
5d248cad89 prep release 2019-08-13 12:28:05 -07:00
b1cb72d088 update url crate 2019-08-13 11:03:24 -07:00
55179d6ab2 update dependencies 2019-08-13 10:48:11 -07:00
192dfff680 prepare actix-http 0.2.9 release 2019-08-13 15:20:29 +02:00
915010e733 Fixes a bug in OpenWaitingConnection where the h2 flow would panic a future (#1031) 2019-08-13 14:55:04 +02:00
dbe4c9ffb5 Replace deprecated methods in actix_files (#1027)
* Bump up mime_guess to 2.0.1

* Replace deprecated methods

* Update CHANGE.md
2019-08-12 05:43:29 +09:00
0ee69671ba Update nightly to 2019-08-10 (#1028) 2019-08-12 04:00:13 +09:00
80e1d16ab8 Merge pull request #1023 from lukaslueg/byteorder_removed
Remove byteorder-dependency
2019-08-07 12:28:23 -03:00
b70de5b991 Update CHANGES.md 2019-08-07 16:43:03 +02:00
0b9e692298 Remove byteorder-dependency 2019-08-06 18:32:36 +02:00
cf1a60cb3a prepare awc release 2019-08-01 15:41:14 -07:00
0d15861e23 prepare actix-http release 2019-08-01 15:26:30 -07:00
cb19ebfe0c add rustls support for actix-http and awc (#998)
* add rustls support for actix-http and awc

* fix features conflict

* remove unnecessary duplication

* test server with rust-tls

* fix

* test rustls

* awc rustls test

* format

* tests

* fix dependencies

* fixes and add changes

* remove test-server and Cargo.toml dev-dependencies changes

* cargo fmt
2019-07-31 13:02:56 -07:00
0d9ea41047 update min rust version 2019-07-31 06:49:46 -07:00
e9b4aa205f Merge branch 'master' of github.com:actix/actix-web 2019-07-30 08:00:57 -07:00
7674f1173c fix awc client panic #1016 2019-07-30 08:00:46 -07:00
511026cab0 Allow HeaderMap to be cloned (#1014)
* Allow HeaderMap to be cloned

* Add entry to changelog
2019-07-29 08:11:23 +04:00
81ab37f235 Fix two dyn warnings (#1015) 2019-07-29 08:10:33 +04:00
6f2049ba9b Fix typo 2019-07-25 12:54:59 +01:00
52372fcbea actix-files: "Specified path is not a directory" error now includes the path (#1004) 2019-07-23 06:41:58 +06:00
f3751d83f8 Modify response body only if encoder is not None #997 2019-07-22 11:35:00 +06:00
b0b462581b update CHANGES.md for Form impl Responder 2019-07-20 14:46:46 +01:00
8f48ed2597 impl Responder for Form 2019-07-20 14:46:46 +01:00
c96068e78d bump version 2019-07-20 11:46:21 +06:00
7bca1f7d8d Allow to disable Content-Disposition header #686 2019-07-20 11:43:49 +06:00
3618a84164 update changes 2019-07-20 11:27:21 +06:00
03ca408e94 add support for specifying protocols on websocket handshake (#835)
* add support for specifying protocols on websocket handshake

* separated the handshake function with and without protocols
changed protocols type from Vec<&str> to [&str]
2019-07-20 11:22:06 +06:00
e53e9c8ba3 Add the start_with_addr() function, to obtain an addr to the target websocket actor (#988) 2019-07-20 11:17:58 +06:00
941241c5f0 Remove unneeded actix-utils dependency 2019-07-20 10:50:36 +06:00
f8320fedd8 add note about Query decoding (#992) 2019-07-19 17:37:49 +06:00
c808364c07 make Query payload public (#991) 2019-07-19 15:47:44 +06:00
cccd829656 update changes 2019-07-19 11:07:52 +06:00
3650f6d7b8 Re-implement Host predicate (#989)
* update HostGuard implementation

* update/add tests for new HostGuard implementation
2019-07-19 10:28:43 +06:00
6b7df6b242 prep actix-web release 2019-07-18 17:51:51 +06:00
b6ff786ed3 update dependencies 2019-07-18 17:50:10 +06:00
9c3789cbd0 revert DateServiceInner changes 2019-07-18 17:37:41 +06:00
29098f8397 Add support for downcasting response errors (#986)
* Add support for downcasting response errors

* Added test for error casting
2019-07-18 17:25:50 +06:00
fbdda8acb1 Unix domain sockets (HttpServer::bind_uds) #92 2019-07-18 17:24:12 +06:00
d03296237e Log error results in Logger middleware (closes #938) (#984)
* Log error results in Logger middleware (closes #938)

* Log internal server errors with an ERROR log level

* Logger middleware: don't log 500 internal server errors, as Actix now logs them always

* Changelog
2019-07-18 14:31:18 +06:00
b36fdc46db Remove several usages of 'unsafe' (#968)
* Replace UnsafeCell in DateServiceInner with Cell

The previous API was extremely dangerous - calling `get_ref()`
followed by `reset()` would trigger instant UB, without requiring
any `unsafe` blocks in the caller.

By making DateInner `Copy`, we can use a normal `Cell` instead
of an `UnsafeCell`. This makes it impossible to cause UB (or even panic)
with the API.

* Split unsafe block HttpServiceHandlerResponse

Also add explanation of the safety of the usage of `unsafe`

* Replace UnsafeCell with RefCell in PayloadRef

This ensures that a mistake in the usage of 'get_mut' will cause
a panic, not undefined behavior.
2019-07-18 04:45:17 +06:00
2a2d7f5768 nightly clippy warnings 2019-07-17 15:53:51 +06:00
4092c7f326 clippy warnings 2019-07-17 15:08:30 +06:00
ef3e1037a8 bump version 2019-07-17 14:18:26 +06:00
baaa7b3fbb Replace ClonableService with local copy 2019-07-17 13:55:44 +06:00
32718b7e31 Expose factory traits and some clippy fixes (#983) 2019-07-17 12:58:42 +06:00
c01611d8b5 prepare actix-web release 2019-07-17 12:07:12 +06:00
7b1dcaffda cleanup deprecation warning for Box<dyn> 2019-07-17 11:44:39 +06:00
c65dbaf88e expose app's ResourceMap via resource_map method 2019-07-17 11:33:05 +06:00
c45728ac01 prep test server release 2019-07-16 10:21:52 +06:00
6f71409355 Add DELETE, PATCH, OPTIONS methods to TestServerRunner (#973) 2019-07-16 10:19:28 +06:00
8d17c8651f update bench link 2019-07-11 14:45:58 +06:00
b1143168e5 Impl Responder for (T, StatusCode) where T: Responder (#954) 2019-07-11 14:42:58 +06:00
69456991f6 update api doc example for client and add panic info for connection_info 2019-07-11 14:40:37 +06:00
f410f3330f prepare actix-session release 2019-07-08 23:25:51 +06:00
e1fcd203f8 Update the copyless version to 0.1.4 (#956)
< 0.1.4 failed to check for null when doing allocations which could lead to null dereferences.
2019-07-08 15:48:20 +06:00
0d8a4304a9 Drop a duplicated word (#958) 2019-07-05 20:46:55 +06:00
14cc5a5d6b Merge pull request #912 from Dowwie/master
updated actix-session to support login and logout functionality
2019-07-03 21:07:07 -04:00
287c2b1d18 Merge branch 'master' into master 2019-07-03 18:50:19 -04:00
7596ab69e0 reverted actix-web/CHANGES.md 2019-07-03 08:55:29 -04:00
1fdd77bffa reworded session info in CHANGES 2019-07-03 07:56:50 -04:00
2d424957fb updated version in Cargo to 0.2 2019-07-03 07:50:45 -04:00
dabc4fe00b updated actix-session/CHANGES with info 2019-07-03 07:50:11 -04:00
5bf5b0acd2 updated CHANGES with info about actix-session update 2019-07-03 07:46:46 -04:00
099a8ff7d8 updated session cookie to support login, logout, changes 2019-07-01 15:26:19 -04:00
a28b7139e6 prepare awc release 2019-07-01 11:34:57 +06:00
a0a469fe85 disable travis cargo cache 2019-07-01 11:33:11 +06:00
dbab55dd6b Bump rand crate version to 0.7 (#951) 2019-07-01 09:37:03 +06:00
d2eb1edac3 Actix-web client: Always append a colon after username in basic auth (#949)
* Always append a colon after username in basic auth

* Update CHANGES.md
2019-07-01 09:34:42 +06:00
5901dfee1a Fix link to actix-cors (#950) 2019-06-30 21:30:04 +06:00
0e05b37082 updated cookie session to update on change 2019-06-29 14:24:02 -04:00
37f4ce8604 Fixes typo in docs. (#948)
Small typo in docs.
2019-06-29 10:38:16 +06:00
12b5174850 update deps 2019-06-28 14:46:26 +06:00
b77ed193f7 prepare actix-web release 2019-06-28 14:41:56 +06:00
d286ccb4f5 Add on-connect callback #946 2019-06-28 14:34:26 +06:00
cac162aed7 update actix-http changes 2019-06-28 12:34:43 +06:00
a3a78ac6fb Do not set Content-Length header, let actix-http set it #930 2019-06-28 11:42:20 +06:00
596483ff55 prepare actix-web-actors release 2019-06-28 10:54:23 +06:00
768859513a Expose the max limit for payload sizes in Websocket Actors. #925 (#933)
* Expose the max limit for payload sizes in Websocket Actors.

* Revert to previous not-formatted code.

* Implement WebsocketContext::with_codec and make Codec Copy and Clone.

* Fix formatting.

* Fix formatting.
2019-06-28 10:49:03 +06:00
44bb79cd07 Call req.path() on Json extractor error only (#945)
* Call req.path() on Json extractor error only

* Cleanup len parse code
2019-06-28 10:44:53 +06:00
af9fb5d190 Support asynchronous data factories #850 2019-06-28 10:43:52 +06:00
50a9d9e2c5 Merge branch 'master' into master 2019-06-27 06:38:13 -04:00
c0c71f82c0 Fixes typo. (#940)
Small typo fix.
2019-06-25 23:23:36 +06:00
93855b889a Merge branch 'master' into master 2019-06-24 18:41:48 -04:00
fa7e0fe6df updated cookie.rs req to get_changes 2019-06-24 18:40:14 -04:00
b948f74b54 Extractor configuration Migration (#937)
added guide for Extractor configuration in MIGRATION.md
2019-06-24 07:16:04 +06:00
1a24ff8717 Add builder function for HTTP 429 Too Many Requests status (#931) 2019-06-21 13:06:29 +06:00
47fab0e393 Bump derive_more crate version to 0.15.0 in actix-cors (#927) 2019-06-19 16:41:42 +06:00
313ac48765 Use encoding_rs crate instead of unmaintained encoding crate (#922)
* Use encoding_rs crate instead of unmaintained encoding crate

* Update changelog
2019-06-18 12:43:25 +06:00
d7780d53c9 Fix typo in actix_web::web::Data::get_ref docstring (#921) 2019-06-18 07:27:23 +06:00
ad0e6f73b3 update version 2019-06-17 12:35:00 +06:00
546a8a58db remove cors and identity middlewares 2019-06-17 12:33:00 +06:00
acda1c075a prepare actix-web release 2019-06-17 12:23:30 +06:00
686e5f1595 update deps 2019-06-16 22:10:22 +06:00
d2b6502c7a prepare actix-http release 2019-06-16 21:59:22 +06:00
7c0f570845 Do not compress NoContent (204) responses #918 2019-06-16 21:54:17 +06:00
382d4ca216 Merge branch 'master' into master 2019-06-15 22:21:39 +06:00
eaa371db8b update migration 2019-06-15 22:20:46 +06:00
d293ae2a69 fix nested resource map registration #915 2019-06-15 22:12:20 +06:00
d7ec241fd0 re-export identity and cors middleware 2019-06-15 21:47:06 +06:00
cd323f2ff1 Move cors middleware to actix-cors crate 2019-06-15 09:34:16 +06:00
32a66a99bf reverting change to get_session due to side effects 2019-06-13 09:19:03 -04:00
73ae801a13 Merge branch 'master' of https://github.com/Dowwie/actix-web 2019-06-13 09:00:45 -04:00
ca4ed0932e made Session::get_session public 2019-06-13 08:59:59 -04:00
bf48798bce Content-Length is 0 for NamedFile HEAD request #914 2019-06-13 15:27:21 +06:00
9fc7c8b1af Merge branch 'master' into master 2019-06-12 23:53:36 +06:00
c8118e8411 fix path doc tests 2019-06-12 20:12:15 +06:00
65732197b8 modified so as to consider unanticipated state changes 2019-06-12 10:11:38 -04:00
959eef05ae updated actix-session to support login and logout functionality (renew and purge) 2019-06-12 08:03:27 -04:00
e7ba67e1a8 rename PathPayloadError and test for path config 2019-06-12 17:02:45 +06:00
13e618b128 Added initial support for PathConfig, allows setting custom error handler. (#903) 2019-06-12 16:49:56 +06:00
36e6f0cb4b add "put" and "sput" methods for test server (#909) 2019-06-12 16:47:00 +06:00
7450ae37a7 Re-apply patch from #637 #894 2019-06-12 16:45:05 +06:00
2ffda29f9b Allow to test an app that uses async actors #897 2019-06-12 16:15:06 +06:00
ff724e239d move identity service separate crate 2019-06-12 15:52:48 +06:00
Bob
ee769832cf get_identity from HttpMessage (#908)
* get_identity from HttpMessage

* more doc for RequestIdentity
2019-06-12 09:26:46 +06:00
c4b7980b4f Upgraded actix-web dependency and set default-features to false (#895) 2019-06-07 09:34:56 +06:00
bfbac4f875 Upgraded actix-web dependency and set default-features to false (#900) 2019-06-07 09:34:30 +06:00
53e2f8090f Mark default enabled package features in the docs (#890) 2019-06-06 11:14:56 +06:00
e399e01a22 update readme 2019-06-05 09:02:44 +06:00
d9a62c4bbf add App::register_data() 2019-06-05 08:43:39 +06:00
a548b69679 fmt 2019-06-05 08:43:13 +06:00
ae64475d98 test-server release 2019-06-05 08:27:25 +06:00
a342b1289d prep awc release 2019-06-05 08:14:00 +06:00
38f04b75a7 update deps 2019-06-04 22:36:10 +06:00
a771540b16 prepare actix-web-codegen release 2019-06-04 22:33:43 +06:00
cf217d35a8 Added HEAD, CONNECT, OPTIONS and TRACE to the codegen (#886)
* Added HEAD, CONNECT, OPTIONS and TRACE to the codegen

* Add new macros to use statement

* Add patch to supported codegen http methods

* Update CHANGES.md

Added head, options, trace, connect and patch codegen changes to CHANGES.md
2019-06-04 22:30:43 +06:00
0e138e111f add external resource support on scope level 2019-06-03 23:41:32 +06:00
1fce4876f3 Scope configuration (#880)
* WIP: Scope configuarion

* Extensions: Fix into_iter()

* Scope: Fix tests

* Add ScopeConfig to web

Committing from mobile, if this doesn't look good it's because I haven't tested it...

* Scope Config: Use ServiceConfig instead

* Scope: Switch to ServiceConfig in doc

* ScopeConfig: Remove unnecessary changes, handle the case when data is empty

* ScopeConfig: Remove changes from actix-http
2019-06-03 23:12:37 +06:00
4a179d1ae1 prepare actix-session release 2019-06-03 10:52:43 +06:00
a780ea10e9 Guard cookie mod by cookie-session feature (#883)
Signed-off-by: Igor Gnatenko <i.gnatenko.brain@gmail.com>
2019-06-03 10:30:30 +06:00
6d2e190c8e prepare actix-files release 2019-06-02 13:09:21 +06:00
b1cfbdcf7a prepare actix-http release 2019-06-02 13:05:22 +06:00
24180f9014 Fix boundary parsing #876 2019-06-02 12:58:37 +06:00
15cdc680f6 Static files are incorrectly served as both chunked and with length #812 2019-06-01 17:57:40 +06:00
666756bfbe body helpers 2019-06-01 17:57:25 +06:00
a1b40f4314 add license files 2019-06-01 17:25:29 +06:00
29a0fe76d5 prepare actix-web-codegen release 2019-06-01 17:21:22 +06:00
7753b9da6d web-codegen: Add extra-traits to syn features (#879)
```rust
error[E0277]: `syn::attr::NestedMeta` doesn't implement `std::fmt::Debug`
   --> src/route.rs:149:57
    |
149 |                 attr => panic!("Unknown attribute{:?}", attr),
    |                                                         ^^^^ `syn::attr::NestedMeta` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug`
    |
    = help: the trait `std::fmt::Debug` is not implemented for `syn::attr::NestedMeta`
    = note: required because of the requirements on the impl of `std::fmt::Debug` for `&syn::attr::NestedMeta`
    = note: required by `std::fmt::Debug::fmt`
```
2019-06-01 14:13:45 +06:00
f1764bba43 Fix Logger time format (use rfc3339) (#867)
* Fix Logger time format (use rfc3339)

* Update change log
2019-05-31 12:09:21 +04:00
c2d7db7e06 prepare actix-web-actors release 2019-05-29 16:22:57 -07:00
21418c7414 prep actix-http release 2019-05-29 16:15:12 -07:00
fe781345d5 Add Migration steps for Custom Error (#869)
Adds migration steps for custom error in 1.0
2019-05-29 20:47:04 +04:00
a614be7cb5 Don't DISCONNECT from stream when reader is empty (#870)
* Don't DISCONNECT from stream when reader is empty

* Fix chunked transfer: poll_request before closing stream + Test
2019-05-29 20:37:42 +04:00
1eb89b8375 remove debug prints 2019-05-25 03:16:53 -07:00
aa626a1e72 handle disconnects 2019-05-25 03:16:46 -07:00
7f12b754e9 Handle socket read disconnect 2019-05-25 03:07:40 -07:00
3f196f469d update version 2019-05-25 02:13:04 -07:00
35eb378585 prepare actix-files release 2019-05-25 02:02:28 -07:00
6db625f55b Update actix-web dep to 1.0.0-rc (#864) 2019-05-25 01:52:23 -07:00
801cc2ed5d Cleaned unnecessary Option<_> around ServerBuilder in server.rs/HttpServer (#863) 2019-05-23 05:21:02 -07:00
ded1e86e7e Add ServiceRequest::set_payload() method 2019-05-22 21:25:51 -07:00
babf48c550 fix NamedFile last-modified check #820 2019-05-22 21:21:12 -07:00
d3e807f6e9 move Payload to inner http request 2019-05-22 11:49:27 -07:00
7746e785c1 re-export Service and Transform traits 2019-05-22 11:20:37 -07:00
4e141d7f5d Merge branch 'master' of github.com:actix/actix-web 2019-05-22 11:18:42 -07:00
12842871fe Clear http requests pool on app service drop #860 2019-05-22 11:18:33 -07:00
fc85ae4014 small documentation fix (#856) 2019-05-21 10:43:18 -07:00
5826f39dbe Add set_json method to TestRequest (#851)
- Takes a type which implements serde::Serialize, serializes it to JSON,
and sets it as the payload. The content-type is also set to JSON.
2019-05-18 19:36:28 -07:00
8ff56d7cd5 prepare actix-session release 2019-05-18 11:20:09 -07:00
0843bce7ba prepare actix-multipart 2019-05-18 11:15:58 -07:00
dea0e0a721 update actix-server dep 2019-05-18 11:00:33 -07:00
e857ab1f81 HttpServer::shutdown_timeout u16 to u64 (#849)
Increase maximum graceful shutdown time from 18 hours.

For issue #848.
2019-05-18 10:50:35 -07:00
0dda4b06ea prepare release 2019-05-18 10:49:59 -07:00
cbe0226177 update changes 2019-05-18 10:47:08 -07:00
e8c8626878 update deps 2019-05-18 09:54:23 -07:00
4b215e0839 Support Query<T>::from_query() (#846) 2019-05-17 13:10:46 -07:00
e1ff3bf8fa fix resource match with params #841 2019-05-15 10:31:40 -07:00
80f4ef9aac When using codegen with paths that have parameters then only the first endpoint resolves (#842) 2019-05-15 09:21:07 -07:00
bba90d7f22 Query config (#839)
* add QueryConfig

* expose QueryConfig in web module

* fmt

* use associated type for QueryConfig

* update CHANGES.md
2019-05-14 13:54:30 -07:00
f8af3b86e5 export set_date 2019-05-14 08:48:11 -07:00
6c3d8b8738 Make JsonConfig send (#830)
* replace Rc with Arc

* add Send trait requirement for Fn in JsonConfig error handler

* add Sync trait requirement for Fn in JsonConfig error handler

* use associated type inside JsonConfig

* fix lint: members in the impl has the same order in the trait

* Update CHANGES.md
2019-05-12 20:04:08 -07:00
5a90e33bcc update deps 2019-05-12 12:01:24 -07:00
86b569e320 version 2019-05-12 11:56:01 -07:00
2350a2dc68 Handle cancellation of uploads #834 #736 2019-05-12 11:43:05 -07:00
36d017dcc6 update deps 2019-05-12 11:41:43 -07:00
3bb081852c prep actix-session release 2019-05-12 10:53:21 -07:00
1ca58e876b prepare beta4 release 2019-05-12 10:49:21 -07:00
e9cbcbaf03 update dependencies 2019-05-12 10:18:02 -07:00
07c9eec803 prepare awc release 2019-05-12 10:04:38 -07:00
beae9ca0f7 update changes 2019-05-12 09:57:16 -07:00
07b9707ca1 prepare actix-http release 2019-05-12 09:56:55 -07:00
45c05978b0 Allow to set/override app data on scope level 2019-05-12 09:42:05 -07:00
df08baf67f update actix-net dependencies 2019-05-12 08:34:51 -07:00
4066375737 Update CHANGES.md 2019-05-10 14:45:30 -07:00
a77b0b054a Make App::configure take an FnOnce (#825) 2019-05-10 14:44:49 -07:00
a17ff492a1 fix formatting 2019-05-04 22:18:59 -07:00
33b4c05557 add payload stream migration entry 2019-05-04 22:18:02 -07:00
005c055a7f prepare actix-web release 2019-05-04 20:05:20 -07:00
3d1af19080 prepare actix-http release 2019-05-04 19:51:13 -07:00
fa78da8156 unify route and app data, it allows to provide global extractor config #775 2019-05-04 19:43:49 -07:00
01cfcf3b75 update changes 2019-05-04 08:42:27 -07:00
7ef4f5ac0b Make request headers optional in CORS preflight (#816) 2019-05-04 08:41:37 -07:00
fc19ce41c4 Clean up response extensions in response pool #817 2019-05-03 15:26:34 -07:00
6e00eef63a awc: Fix typo on ResponseError documentation (#815)
* awc: Fix typo on ResponseError documentation

Signed-off-by: Otavio Salvador <otavio@ossystems.com.br>

* http: Fix typo on ResponseError documentation

Signed-off-by: Otavio Salvador <otavio@ossystems.com.br>

* http: Expand type names for openssl related errors documentation

Signed-off-by: Otavio Salvador <otavio@ossystems.com.br>
2019-05-03 14:30:00 -07:00
337c2febe3 add more tests 2019-05-02 09:49:10 -07:00
f27beab016 fix case for transfer-encoding header name 2019-05-02 09:30:00 -07:00
4f1c6d1bb7 Update MIGRATION.md (#811) 2019-05-02 09:26:51 -07:00
6b34909537 Fix NormalizePath middleware impl #806 2019-05-01 12:40:56 -07:00
87284f0951 Add doctest to verify NormalizePath middleware (#809) 2019-05-01 11:47:51 -07:00
24bd5b1344 update readmes 2019-04-29 20:47:21 -07:00
94a0d1a6bc remove old api doc refs 2019-04-29 18:42:21 -07:00
f4e1205cbb fix reactor drop panic 2019-04-29 10:14:08 -07:00
d2c1791067 add async handler test with blocking call 2019-04-29 09:45:37 -07:00
f4b4875cb1 Add helper function for executing futures test::block_fn() 2019-04-29 09:34:14 -07:00
29a841529f Allow to construct Data instances to avoid double Arc for Send + Sync types. 2019-04-29 09:26:12 -07:00
b51b5b763c added clarification to docs regarding middleware processing sequence, added delete method to TestRequest (#799)
* added clarification to docs regarding middleware processing sequnce

* added delete method to TestRequest, doc, and test
2019-04-29 09:14:36 -07:00
8db6b48a76 update version 2019-04-28 09:09:18 -07:00
ffd2c04cd3 Add helper trait UserSession which allows to get session for ServiceRequest and HttpRequest 2019-04-28 09:08:51 -07:00
70a4c36496 use Error explicitly 2019-04-25 11:14:32 -07:00
cba78e06ae update changes 2019-04-24 15:42:34 -07:00
3b3dbb4f40 add raw services support 2019-04-24 15:29:15 -07:00
7300002226 grammar fixes (#796) 2019-04-24 13:21:42 -07:00
5426413cb6 update dependencies 2019-04-24 13:00:30 -07:00
2bc937f6c3 prepare release 2019-04-24 12:50:44 -07:00
60fa0d5427 Store visit and login timestamp in the identity cookie (#502)
This allows to verify time of login or last visit and therfore limiting
the danger of leaked cookies.
2019-04-24 12:49:56 -07:00
f429d3319f Read until eof for http/1.0 responses #771 2019-04-24 11:57:40 -07:00
2e19f572ee add tests for camel case headers rendering 2019-04-24 11:27:57 -07:00
64f603b076 Support to set header names of ClientRequest as Camel-Case (#713)
* Support to set header names of `ClientRequest` as Camel-Case

This is the case for supporting to request for servers which don't
perfectly implement the `RFC 7230`. It is important for an app
which uses `ClientRequest` as core part.

* Add field `upper_camel_case_headers` to `ClientRequest`.

* Add function `set_upper_camel_case_headers` to `ClientRequest`
  and `ClientRequestBuilder` to set field `upper_camel_case_headers`.

* Add trait `client::writer::UpperCamelCaseHeader` for
  `http::header::HeaderName`, let it can be converted to Camel-Case
  then writed to buffer.

* Add test `test_client::test_upper_camel_case_headers`.

* Support upper Camel-Case headers

* [actix-http] Add field `upper_camel_case_headers` for `RequestHead`
* [actix-http] Add code for `MessageType` to support upper camel case
* [awc] Add functions for `ClientRequest` to set upper camel case

* Use `Flags::CAMEL_CASE` for upper camel case of headers
2019-04-24 10:48:49 -07:00
679d1cd513 allow to override responder's status code and headers 2019-04-24 10:25:46 -07:00
42644dac3f prepare actix-http-test release 2019-04-24 07:31:33 -07:00
898ef57080 Fix async web::Data factory handling 2019-04-23 21:21:49 -07:00
9702b2d88e add client h2 reuse test 2019-04-23 15:06:30 -07:00
d2b0afd859 Fix http client pool and wait queue management 2019-04-23 14:57:03 -07:00
5f6a1a8249 update version 2019-04-23 09:45:39 -07:00
5d531989e7 Fix BorrowMutError panic in client connector #793 2019-04-23 09:42:19 -07:00
3532602299 Added support for remainder match (i.e /path/{tail}*) 2019-04-22 21:22:17 -07:00
48bee55087 .to_async() handler can return Responder type #792 2019-04-22 14:22:08 -07:00
d00c9bb844 do not consume boundary 2019-04-21 16:14:09 -07:00
895e409d57 Optimize multipart handling #634, #769 2019-04-21 15:41:01 -07:00
f0789aad05 update dep versions 2019-04-21 09:03:46 -07:00
7e480ab2f7 beta.1 release 2019-04-20 21:16:51 -07:00
891f857547 update changes 2019-04-20 11:18:04 -07:00
01b1350dcc update versions 2019-04-19 18:16:01 -07:00
5e4e95fb0a update create version 2019-04-19 18:13:05 -07:00
9f421b81b8 fix non-ssl connector 2019-04-19 18:10:53 -07:00
6decfdda1f update deps 2019-04-19 18:06:34 -07:00
fc9b14a933 allow to specify server address for http and ws requests 2019-04-19 18:03:44 -07:00
7292d0b696 drop chrono and use i64 for max age 2019-04-19 17:23:17 -07:00
a3844c1bfd update version 2019-04-19 13:55:36 -07:00
791f22bbc8 replate time::Duration with chrono::Duration and add max_age_time method (#789)
* feat: replate time::Duration with chrono::Duration

* feat: rename max_age method which accepts `Duration` to max_age_time and add new max_age method accepting isize of seconds

* feat: replace `time:Duration` with `chrono:Duration` in repo `actix-http`
2019-04-19 13:54:44 -07:00
1e7f97a111 Add Normalization middleware for in place (#783) 2019-04-19 13:53:49 -07:00
bc40f5ae40 Merge pull request #788 from Dowwie/master
added put and patch to TestRequest, docs, and test
2019-04-19 06:55:44 -04:00
3504a8fc0a Merge branch 'master' into master 2019-04-19 04:38:41 -04:00
bfe0df5ab0 update tests 2019-04-18 21:28:23 -07:00
ed94df189f Merge branch 'master' into master 2019-04-18 19:03:48 -04:00
aa255298ef make ServiceRequest::from_parts private, as it is not safe to create from parts 2019-04-18 16:03:13 -07:00
da86b6e062 added put and patch to TestRequest, docs, and test 2019-04-18 18:06:32 -04:00
75e340137d use local version of http-test 2019-04-18 12:23:56 -07:00
e659e09e29 update tests 2019-04-18 11:01:04 -07:00
163ca89cf4 more tests 2019-04-17 17:48:25 -07:00
85b598a614 add cookie session test 2019-04-17 11:02:03 -07:00
b64851c5ec enable runtime for test:: methods 2019-04-17 10:28:27 -07:00
cc8420377e pass request ownership to closure instead of ref 2019-04-16 15:43:55 -07:00
5740f1e63a prepare actix-framed release 2019-04-16 11:18:47 -07:00
c943e95812 update dependencies 2019-04-16 11:17:29 -07:00
4c0ebd55d3 prepare actix-http-test release 2019-04-16 11:02:26 -07:00
e7ec77aa81 update readme 2019-04-16 10:50:37 -07:00
ddfd7523f7 prepare awc release 2019-04-16 10:49:38 -07:00
2986077a28 no need for feature 2019-04-16 10:32:48 -07:00
3744957804 actix_http::encoding always available 2019-04-16 10:27:58 -07:00
420d3064c5 Add .peer_addr() #744 2019-04-16 10:11:38 -07:00
eb4f6b74fb Merge branch 'master' of github.com:actix/actix-web 2019-04-16 09:58:07 -07:00
a116c4c2c7 Expose peer addr via Request::peer_addr() and RequestHead::peer_addr 2019-04-16 09:54:02 -07:00
7f674febb1 add 422 to httpcodes.rs (#782) 2019-04-15 16:55:06 -07:00
14252f5ef2 use test::call_service 2019-04-15 09:09:21 -07:00
7a28b32f6d Rename test::call_success to test::call_service 2019-04-15 07:44:07 -07:00
09cdf1e302 Rename RouterConfig to ServiceConfig 2019-04-15 07:32:49 -07:00
1eebd47072 fix warnings 2019-04-14 21:00:16 -07:00
002c41a7ca update trust-dns 2019-04-14 20:45:44 -07:00
ab4fda6084 update tests 2019-04-14 20:20:33 -07:00
f9078d41cd add test::read_response; fix TestRequest::app_data() 2019-04-14 19:52:12 -07:00
4cc2b38059 added read_response_json for testing (#776)
* added read_response_json for testing

* cleaned up

* modied docs for read_response_json

* typo in doc

* test code in doc should compile now

* use type coercion in doc

* removed generic R, replaced with Request
2019-04-14 16:25:45 -07:00
d7040dc303 alpha.6 release 2019-04-14 08:09:32 -07:00
6bc1a0c76b Do not set default headers for websocket request 2019-04-14 07:43:53 -07:00
5bd5651faa Allow to use any service as default service 2019-04-13 22:25:00 -07:00
32ac159ba2 update migration 2019-04-13 16:51:41 -07:00
ee33f52736 make extractor config type explicit 2019-04-13 16:35:25 -07:00
4f30fa9d46 Remove generic type for request payload, always use default 2019-04-13 14:50:54 -07:00
043f6e77ae remove nested multipart support 2019-04-13 10:11:07 -07:00
48518df883 do not generate all docs; use docs.rs for 1.0 docs 2019-04-13 09:35:23 -07:00
1f2b15397d prepare alpha5 release 2019-04-12 14:00:45 -07:00
87167f6581 update actix-connect 2019-04-12 12:33:11 -07:00
b4768a8f81 add TestRequest::run(), allows to run async functions 2019-04-12 11:28:57 -07:00
3fb7343e73 provide during test request construction 2019-04-12 11:22:18 -07:00
5cfba5ff16 add FramedRequest builder for testing 2019-04-12 11:15:58 -07:00
67c34a5937 Add Debug impl for BoxedSocket 2019-04-11 16:01:54 -07:00
94d7a7f873 custom future for SendError service 2019-04-11 15:12:23 -07:00
d86567fbdc revert SendResponse::Error type 2019-04-11 14:18:58 -07:00
d115b3b3ed ws verifyciation takes RequestHead; add SendError utility service 2019-04-11 14:00:32 -07:00
6420a2fe1f update client example 2019-04-10 20:57:18 -07:00
0eed9e5257 add more migration 2019-04-10 20:51:57 -07:00
7801fcb993 update migration 2019-04-10 20:47:28 -07:00
e55be4dba6 add FramedRequest helper methods 2019-04-10 19:57:34 -07:00
12e1dad42e export TestBuffer 2019-04-10 19:43:09 -07:00
7cd59c38d3 rename framed App 2019-04-10 18:08:28 -07:00
8dc4a88aa6 add actix-framed 2019-04-10 15:06:27 -07:00
52aebb3bca fmt 2019-04-10 15:05:03 -07:00
6b42b2aaee remove framed for now 2019-04-10 12:55:56 -07:00
6ab9838977 added some error logging for extractors: Data, Json, Query, and Path (#765)
* added some error logging for extractors

* changed log::error to log::debug and fixed position of log for path

* added request path to debug logs
2019-04-10 12:45:13 -07:00
9d82d4dfb9 Fix body propagation in Response::from_error. #760 2019-04-10 12:43:31 -07:00
9bb40c249f add h1::SendResponse future; renamed to MessageBody::size 2019-04-10 12:24:17 -07:00
046b7a1425 Expand codegen to allow specify guards and async 2019-04-10 15:43:18 +03:00
c22a3a71f2 fix test 2019-04-08 19:07:11 -07:00
9c9940d88d update readme 2019-04-08 17:53:19 -07:00
561f83d044 add upgrade service support to h1 dispatcher 2019-04-08 17:51:14 -07:00
43d325a139 allow to specify upgrade service 2019-04-08 14:51:16 -07:00
0a6dd0efdf fix compression tests 2019-04-08 12:48:39 -07:00
b921abf18f set host header for http1 connections 2019-04-08 12:48:26 -07:00
9bcd5d6664 updated legacy code in call_success example (#762) 2019-04-08 11:20:46 -07:00
bc58dbb2f5 add async expect service test 2019-04-08 11:19:56 -07:00
b1547bbbb6 do not set default headers 2019-04-08 11:09:57 -07:00
a7fdac1043 fix expect service registration and tests 2019-04-08 10:31:29 -07:00
53da55aa3c alpha4 release 2019-04-07 23:42:05 -07:00
aa78565453 use objects pool for HttpRequest; optimize nested services call 2019-04-07 23:06:21 -07:00
75b213a6f0 refactor FromRequest trait 2019-04-07 14:43:07 -07:00
3c650ca194 remove buffer capacity for payload 2019-04-07 10:40:45 -07:00
219baf3323 remove PayloadWriter trait 2019-04-07 10:29:26 -07:00
ec09d6fbe6 optimize encode headers and body split 2019-04-07 10:03:38 -07:00
68d2203dd6 run travis with stable rust only 2019-04-07 08:17:29 -07:00
748289f0ff use custom headers map; more optimizations 2019-04-06 15:02:02 -07:00
4ef46e26f9 Merge branch 'master' of github.com:actix/actix-web 2019-04-06 08:13:14 -07:00
3872d3ba5a refactor h1 dispatcher 2019-04-06 08:12:58 -07:00
b1523ab78c started 1.0 migration guide (#758) 2019-04-06 07:39:20 -07:00
fbedaec661 add expect: 100-continue support #141 2019-04-05 16:46:44 -07:00
02fcaca3da add backward compatibility 2019-04-05 11:36:26 -07:00
18593d8476 updated Connector docs and renamed service() to finish() (#757)
* added Connector to actix-web::client namespace

* updated Connector, renaming service() to finish() and adding docs

* added doc for finish method on Connector
2019-04-05 11:34:27 -07:00
b6dacaa23a remove SendError and SendResponse services 2019-04-05 11:29:42 -07:00
f89321fd01 fix import 2019-04-05 10:50:11 -07:00
0d4a8e1b1c update actix-connect 2019-04-05 10:35:14 -07:00
162cd3eecd added Connector to actix-web::client namespace (#756) 2019-04-05 07:37:00 -07:00
a655bdac52 Fix clippy warning (#755) 2019-04-05 12:34:24 +03:00
309c480782 encoder sent uncompressed data before compressed 2019-04-04 15:03:40 -07:00
9c205f9f1d update tests for content-encoding 2019-04-04 14:00:56 -07:00
1f5c0f50f9 Add minimal std::error::Error impl for Error 2019-04-04 13:23:38 -07:00
d8bc66a18e Use thread pool for response body comression 2019-04-04 13:17:55 -07:00
bc834f6a03 remove some static contraints 2019-04-04 10:59:34 -07:00
dc7c3d37a1 upgrade router 2019-04-03 21:45:30 -07:00
1e2bd68e83 Render error and return as response body 2019-04-03 19:55:19 -07:00
954fe21751 set response error body 2019-04-03 19:07:25 -07:00
7d6085ddbd Add %U (URLPath) for logger (#752)
* Add %R (Route) for logger

* Requested Updates (Route => URLPath, %R => %U)
2019-04-03 17:41:42 -07:00
cef3dc3586 added app_data() method 2019-04-03 15:25:52 -07:00
237bfba1ed add App::configure() - allow to offload app configuration to different methods 2019-04-03 15:09:31 -07:00
dfa0abf5a5 Export IntoHeaderValue 2019-04-03 12:44:47 -07:00
e738361e09 move multipart support to separate crate 2019-04-03 12:28:58 -07:00
f56072954b remove PayloadBuffer 2019-04-03 03:20:20 -07:00
2a89b995aa do not cleanup travis build 2019-04-02 21:56:38 -07:00
442f5057dd alpha.3 release 2019-04-02 21:49:31 -07:00
19eef36f8f Merge branch 'tarpaulin' 2019-04-02 21:11:03 -07:00
51d5006ccf Detect socket disconnection during protocol selection 2019-04-02 20:50:25 -07:00
3aebe09e5c travis 2019-04-02 19:21:22 -07:00
4227cddd30 fix dev dependencies 2019-04-02 15:00:10 -07:00
db1f7651a3 more patch cratesio 2019-04-02 14:47:59 -07:00
00000fb316 mut obj 2019-04-02 14:27:54 -07:00
f100976ef0 rename close_connection to force_close 2019-04-02 14:08:30 -07:00
deac983bc7 fix test-server workspace setup 2019-04-02 14:04:28 -07:00
bca31eb7ad remove Deref 2019-04-02 13:35:01 -07:00
e282ef7925 return back consuming builder 2019-04-02 12:51:16 -07:00
49a499ce74 properly allocate read buffer 2019-04-02 11:11:32 -07:00
d067b1d5f1 do not use static 2019-04-02 10:53:44 -07:00
c27fbdc35f Preallocate read buffer for h1 codec, #749 2019-04-02 10:19:56 -07:00
1bd0995d7a remove unneded & 2019-04-01 18:00:38 -07:00
2d43489278 ClientRequest::json() accepts reference instead of object 2019-04-01 17:53:30 -07:00
89a0a50e14 Merge branch 'master' of github.com:actix/actix-web 2019-04-01 15:20:04 -07:00
38afc93304 Use non-consuming builder pattern for ClientRequest 2019-04-01 15:19:34 -07:00
03c84be1f2 Merge pull request #750 from Dowwie/master
added docs for wrap and wrap_fn
2019-04-01 17:37:04 -04:00
6d169f4c9c Merge branch 'master' of https://github.com/Dowwie/actix-web 2019-04-01 15:10:49 -04:00
3dd3f7bc92 updated scope wrap doc 2019-04-01 15:10:28 -04:00
e6936d9f73 Merge branch 'master' into master 2019-04-01 14:53:23 -04:00
03dfbdfcdd updated wrap and wrap fn descriptions, still requiring viable examples 2019-04-01 14:52:05 -04:00
5c4e4edda4 add ClientResponse::json() 2019-04-01 11:51:18 -07:00
c5fa6c1abe do not consume response 2019-04-01 11:29:50 -07:00
6c195d8521 add Derev<Target=RequestHead> for ClientRequest 2019-04-01 10:26:25 -07:00
96fd61f3d5 rust 1.31.0 compatibility 2019-04-01 10:26:09 -07:00
8800b8ef13 mentioned re-use in wrap doc 2019-04-01 09:59:21 -04:00
220c04b7b3 added docs for wrap and wrap_fn 2019-04-01 09:30:11 -04:00
34695f4bce rename test methods; update tests 2019-03-31 20:43:00 -07:00
15c5a3bcfb fix test 2019-03-31 18:57:54 -07:00
ab45974e35 add default handler 2019-03-31 18:19:18 -07:00
e4b3f79458 allocate enough space 2019-03-31 17:05:02 -07:00
ce8294740e fix tests with disabled features 2019-03-31 17:04:34 -07:00
ddf5089bff Warn when an unsealed private cookie isn't valid UTF-8 (#746) 2019-03-31 16:26:56 +03:00
7596d0b7cb fix fn_guard doc string 2019-03-30 20:48:00 -07:00
1a871d708e update guard doc test 2019-03-30 12:13:21 -07:00
351df84cca update stable release api doc link 2019-03-30 11:37:56 -07:00
6fcbe4bcda add fn_guard 2019-03-30 11:33:31 -07:00
457b75c995 update api docs; move web to submodule 2019-03-30 10:04:38 -07:00
724e9c2efb replace deprecated fn 2019-03-30 07:56:09 -07:00
2e159d1eb9 test-server: Request functions should accept path (#743) 2019-03-30 07:53:45 -07:00
a20b9fd354 prepare aplha2 release 2019-03-29 22:06:14 -07:00
d846328f36 fork cookie crate 2019-03-29 21:13:39 -07:00
193f8fb2d9 update tests 2019-03-29 18:51:07 -07:00
3a954298d7 Merge branch 'master' of github.com:actix/actix-web 2019-03-29 18:23:07 -07:00
3220777ff9 Added ws::Message::Nop, no-op websockets message 2019-03-29 18:22:49 -07:00
00526f60dc Impl BodyEncoding for Response (#740) 2019-03-29 16:29:11 -07:00
c126713f40 add rustls support to HttpServer 2019-03-29 16:28:32 -07:00
e9bbde6832 allow to override request's uri 2019-03-29 16:28:28 -07:00
5eb3f1154e revert 2019-03-29 14:27:22 -07:00
aebeb511cd explicit impl traits for ws connect 2019-03-29 14:26:11 -07:00
744d82431d add per request timeout 2019-03-29 14:07:37 -07:00
058b1d56e6 Export ws sub-module with websockets related types 2019-03-29 13:49:21 -07:00
709475b2bb multipart::Field renamed to MultipartField 2019-03-29 11:59:38 -07:00
19a0b8046b remove actix reference 2019-03-29 11:13:36 -07:00
1e7096a63a add request timeout 2019-03-28 22:33:41 -07:00
ea4d98d669 Session wide headers, basic and bearer auth 2019-03-28 21:48:35 -07:00
3b897da8e2 Do not use thread pool for decomression if chunk size is smaller than 2048 2019-03-28 21:15:26 -07:00
10b166404e Merge branch 'master' of github.com:actix/actix-web 2019-03-28 20:27:59 -07:00
80ff7d40a1 enable awc/ssl if ssl features is enabled 2019-03-28 20:27:47 -07:00
c4a8bbe47b fix the example in README.md (#739) 2019-03-28 20:03:17 -07:00
9710e9b01f Re-export actix_http::client::Connector 2019-03-28 14:46:33 -07:00
1d79f16529 update release api docs link 2019-03-28 14:30:38 -07:00
670a457013 fix docs.rs feature list 2019-03-28 14:28:59 -07:00
878f32c495 fix tests for no-default-features 2019-03-28 14:27:07 -07:00
a2c9ff3a33 back to development 2019-03-28 14:10:03 -07:00
9c198a0d29 alpha.1 release 2019-03-28 13:46:26 -07:00
231 changed files with 29697 additions and 13043 deletions

67
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,67 @@
name: CI
on: [push, pull_request]
env:
VCPKGRS_DYNAMIC: 1
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
toolchain:
- x86_64-pc-windows-msvc
# - i686-pc-windows-msvc
- x86_64-apple-darwin
version:
- stable
- nightly
include:
- toolchain: x86_64-pc-windows-msvc
os: windows-latest
arch: x64
# - toolchain: i686-pc-windows-msvc
# os: windows-latest
# arch: x86
- toolchain: x86_64-apple-darwin
os: macOS-latest
name: ${{ matrix.version }} - ${{ matrix.toolchain }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.toolchain }}
default: true
- name: Install OpenSSL
if: matrix.os == 'windows-latest'
run: |
vcpkg integrate install
vcpkg install openssl:${{ matrix.arch }}-windows
- name: check nightly
if: matrix.version == 'nightly'
uses: actions-rs/cargo@v1
with:
command: check
args: --all --benches --bins --examples --tests
- name: check stable
if: matrix.version == 'stable'
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
if: matrix.toolchain != 'x86_64-pc-windows-gnu'
uses: actions-rs/cargo@v1
with:
command: test
args: --all --all-features -- --nocapture

View File

@ -3,16 +3,16 @@ sudo: required
dist: trusty dist: trusty
cache: cache:
cargo: true # cargo: true
apt: true apt: true
matrix: matrix:
include: include:
- rust: stable - rust: stable
- rust: beta - rust: beta
- rust: nightly-2019-03-02 - rust: nightly-2019-11-20
allow_failures: allow_failures:
- rust: nightly-2019-03-02 - rust: nightly-2019-11-20
env: env:
global: global:
@ -25,8 +25,8 @@ before_install:
- sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev
before_cache: | before_cache: |
if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-11-20" ]]; then
RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install --version 0.6.11 cargo-tarpaulin
fi fi
# Add clippy # Add clippy
@ -34,21 +34,27 @@ before_script:
- export PATH=$PATH:~/.cargo/bin - export PATH=$PATH:~/.cargo/bin
script: script:
- cargo clean - cargo update
- cargo test --all-features --all -- --nocapture - cargo check --all --no-default-features
- |
if [[ "$TRAVIS_RUST_VERSION" == "stable" || "$TRAVIS_RUST_VERSION" == "beta" ]]; then
cargo test --all-features --all -- --nocapture
cd actix-http; cargo test --no-default-features --features="rustls" -- --nocapture; cd ..
cd awc; cargo test --no-default-features --features="rustls" -- --nocapture; cd ..
fi
# Upload docs # Upload docs
after_success: after_success:
- | - |
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then
cargo doc --all-features --no-deps && cargo doc --no-deps --all-features &&
echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html && echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html &&
git clone https://github.com/davisp/ghp-import.git && git clone https://github.com/davisp/ghp-import.git &&
./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc &&
echo "Uploaded documentation" echo "Uploaded documentation"
fi fi
- | - |
if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-11-20" ]]; then
taskset -c 0 cargo tarpaulin --out Xml --all --all-features taskset -c 0 cargo tarpaulin --out Xml --all --all-features
bash <(curl -s https://codecov.io/bash) bash <(curl -s https://codecov.io/bash)
echo "Uploaded code coverage" echo "Uploaded code coverage"

View File

@ -1,7 +1,411 @@
# Changes # Changes
## [1.0.0-alpha.1] - 2019-03-x ## [2.0.0] - 2019-12-25
### Changed ### Changed
* Rename `HttpServer::start()` to `HttpServer::run()`
* Allow to gracefully stop test server via `TestServer::stop()`
* Allow to specify multi-patterns for resources
## [2.0.0-rc] - 2019-12-20
### Changed
* Move `BodyEncoding` to `dev` module #1220
* Allow to set `peer_addr` for TestRequest #1074
* Make web::Data deref to Arc<T> #1214
* Rename `App::register_data()` to `App::app_data()`
* `HttpRequest::app_data<T>()` returns `Option<&T>` instead of `Option<&Data<T>>`
### Fixed
* Fix `AppConfig::secure()` is always false. #1202
## [2.0.0-alpha.6] - 2019-12-15
### Fixed
* Fixed compilation with default features off
## [2.0.0-alpha.5] - 2019-12-13
### Added
* Add test server, `test::start()` and `test::start_with()`
## [2.0.0-alpha.4] - 2019-12-08
### Deleted
* Delete HttpServer::run(), it is not useful witht async/await
## [2.0.0-alpha.3] - 2019-12-07
### Changed
* Migrate to tokio 0.2
## [2.0.0-alpha.1] - 2019-11-22
### Changed
* Migrated to `std::future`
* Remove implementation of `Responder` for `()`. (#1167)
## [1.0.9] - 2019-11-14
### Added
* Add `Payload::into_inner` method and make stored `def::Payload` public. (#1110)
### Changed
* Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129)
## [1.0.8] - 2019-09-25
### Added
* Add `Scope::register_data` and `Resource::register_data` methods, parallel to
`App::register_data`.
* Add `middleware::Condition` that conditionally enables another middleware
* Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload`
* Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path,
which is useful for example with systemd.
### Changed
* Make UrlEncodedError::Overflow more informativve
* Use actix-testing for testing utils
## [1.0.7] - 2019-08-29
### Fixed
* Request Extensions leak #1062
## [1.0.6] - 2019-08-28
### Added
* Re-implement Host predicate (#989)
* Form immplements Responder, returning a `application/x-www-form-urlencoded` response
* Add `into_inner` to `Data`
* Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set
the header in test requests.
### Changed
* `Query` payload made `pub`. Allows user to pattern-match the payload.
* Enable `rust-tls` feature for client #1045
* Update serde_urlencoded to 0.6.1
* Update url to 2.1
## [1.0.5] - 2019-07-18
### Added
* Unix domain sockets (HttpServer::bind_uds) #92
* Actix now logs errors resulting in "internal server error" responses always, with the `error`
logging level
### Fixed
* Restored logging of errors through the `Logger` middleware
## [1.0.4] - 2019-07-17
### Added
* Add `Responder` impl for `(T, StatusCode) where T: Responder`
* Allow to access app's resource map via
`ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods.
### Changed
* Upgrade `rand` dependency version to 0.7
## [1.0.3] - 2019-06-28
### Added
* Support asynchronous data factories #850
### Changed
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
## [1.0.2] - 2019-06-17
### Changed
* Move cors middleware to `actix-cors` crate.
* Move identity middleware to `actix-identity` crate.
## [1.0.1] - 2019-06-17
### Added
* Add support for PathConfig #903
* Add `middleware::identity::RequestIdentity` trait to `get_identity` from `HttpMessage`.
### Changed
* Move cors middleware to `actix-cors` crate.
* Move identity middleware to `actix-identity` crate.
* Disable default feature `secure-cookies`.
* Allow to test an app that uses async actors #897
* Re-apply patch from #637 #894
### Fixed
* HttpRequest::url_for is broken with nested scopes #915
## [1.0.0] - 2019-06-05
### Added
* Add `Scope::configure()` method.
* Add `ServiceRequest::set_payload()` method.
* Add `test::TestRequest::set_json()` convenience method to automatically
serialize data and set header in test requests.
* Add macros for head, options, trace, connect and patch http methods
### Changed
* Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863
### Fixed
* Fix Logger request time format, and use rfc3339. #867
* Clear http requests pool on app service drop #860
## [1.0.0-rc] - 2019-05-18
### Add
* Add `Query<T>::from_query()` to extract parameters from a query string. #846
* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
### Changed
* `JsonConfig` is now `Send + Sync`, this implies that `error_handler` must be `Send + Sync` too.
### Fixed
* Codegen with parameters in the path only resolves the first registered endpoint #841
## [1.0.0-beta.4] - 2019-05-12
### Add
* Allow to set/override app data on scope level
### Changed
* `App::configure` take an `FnOnce` instead of `Fn`
* Upgrade actix-net crates
## [1.0.0-beta.3] - 2019-05-04
### Added
* Add helper function for executing futures `test::block_fn()`
### Changed
* Extractor configuration could be registered with `App::data()`
or with `Resource::data()` #775
* Route data is unified with app data, `Route::data()` moved to resource
level to `Resource::data()`
* CORS handling without headers #702
* Allow to construct `Data` instances to avoid double `Arc` for `Send + Sync` types.
### Fixed
* Fix `NormalizePath` middleware impl #806
### Deleted
* `App::data_factory()` is deleted.
## [1.0.0-beta.2] - 2019-04-24
### Added
* Add raw services support via `web::service()`
* Add helper functions for reading response body `test::read_body()`
* Add support for `remainder match` (i.e "/path/{tail}*")
* Extend `Responder` trait, allow to override status code and headers.
* Store visit and login timestamp in the identity cookie #502
### Changed
* `.to_async()` handler can return `Responder` type #792
### Fixed
* Fix async web::Data factory handling
## [1.0.0-beta.1] - 2019-04-20
### Added
* Add helper functions for reading test response body,
`test::read_response()` and test::read_response_json()`
* Add `.peer_addr()` #744
* Add `NormalizePath` middleware
### Changed
* Rename `RouterConfig` to `ServiceConfig`
* Rename `test::call_success` to `test::call_service`
* Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts.
* `CookieIdentityPolicy::max_age()` accepts value in seconds
### Fixed
* Fixed `TestRequest::app_data()`
## [1.0.0-alpha.6] - 2019-04-14
### Changed
* Allow to use any service as default service.
* Remove generic type for request payload, always use default.
* Removed `Decompress` middleware. Bytes, String, Json, Form extractors
automatically decompress payload.
* Make extractor config type explicit. Add `FromRequest::Config` associated type.
## [1.0.0-alpha.5] - 2019-04-12
### Added
* Added async io `TestBuffer` for testing.
### Deleted
* Removed native-tls support
## [1.0.0-alpha.4] - 2019-04-08
### Added
* `App::configure()` allow to offload app configuration to different methods
* Added `URLPath` option for logger
* Added `ServiceRequest::app_data()`, returns `Data<T>`
* Added `ServiceFromRequest::app_data()`, returns `Data<T>`
### Changed
* `FromRequest` trait refactoring
* Move multipart support to actix-multipart crate
### Fixed
* Fix body propagation in Response::from_error. #760
## [1.0.0-alpha.3] - 2019-04-02
### Changed
* Renamed `TestRequest::to_service()` to `TestRequest::to_srv_request()`
* Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()`
* Removed `Deref` impls
### Removed
* Removed unused `actix_web::web::md()`
## [1.0.0-alpha.2] - 2019-03-29
### Added
* rustls support
### Changed
* use forked cookie
* multipart::Field renamed to MultipartField
## [1.0.0-alpha.1] - 2019-03-28
### Changed
* Complete architecture re-design.
* Return 405 response if no matching route found within resource #538 * Return 405 response if no matching route found within resource #538

View File

@ -1,10 +1,10 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "1.0.0-alpha.1" version = "2.0.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
readme = "README.md" readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["actix", "http", "web", "framework", "async"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-web/" documentation = "https://docs.rs/actix-web/"
@ -12,12 +12,13 @@ categories = ["network-programming", "asynchronous",
"web-programming::http-server", "web-programming::http-server",
"web-programming::websocket"] "web-programming::websocket"]
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"]
edition = "2018" edition = "2018"
[package.metadata.docs.rs]
features = ["openssl", "rustls", "compress", "secure-cookies"]
[badges] [badges]
travis-ci = { repository = "actix/actix-web", branch = "master" } travis-ci = { repository = "actix/actix-web", branch = "master" }
appveyor = { repository = "fafhrd91/actix-web-hdy9d" }
codecov = { repository = "actix/actix-web", branch = "master", service = "github" } codecov = { repository = "actix/actix-web", branch = "master", service = "github" }
[lib] [lib]
@ -29,91 +30,89 @@ members = [
".", ".",
"awc", "awc",
"actix-http", "actix-http",
"actix-cors",
"actix-files", "actix-files",
"actix-framed",
"actix-session", "actix-session",
"actix-identity",
"actix-multipart",
"actix-web-actors", "actix-web-actors",
"actix-web-codegen", "actix-web-codegen",
"test-server",
] ]
[package.metadata.docs.rs]
features = ["ssl", "tls", "rust-tls", "brotli", "flate2-zlib", "cookies", "client"]
[features] [features]
default = ["brotli", "flate2-zlib", "cookies", "client"] default = ["compress", "failure"]
# http client # content-encoding support
client = ["awc"] compress = ["actix-http/compress", "awc/compress"]
# brotli encoding, requires c compiler
brotli = ["actix-http/brotli2"]
# miniz-sys backend for flate2 crate
flate2-zlib = ["actix-http/flate2-zlib"]
# rust backend for flate2 crate
flate2-rust = ["actix-http/flate2-rust"]
# sessions feature, session require "ring" crate and c compiler # sessions feature, session require "ring" crate and c compiler
cookies = ["cookie", "actix-http/cookies"] secure-cookies = ["actix-http/secure-cookies"]
# tls failure = ["actix-http/failure"]
tls = ["native-tls", "actix-server/ssl"]
# openssl # openssl
ssl = ["openssl", "actix-server/ssl"] openssl = ["actix-tls/openssl", "awc/openssl", "open-ssl"]
# rustls # rustls
# rust-tls = ["rustls", "actix-server/rustls"] rustls = ["actix-tls/rustls", "awc/rustls", "rust-tls"]
[dependencies] [dependencies]
actix-codec = "0.1.1" actix-codec = "0.2.0"
actix-service = "0.3.4" actix-service = "1.0.1"
actix-utils = "0.3.4" actix-utils = "1.0.4"
actix-router = "0.1.0" actix-router = "0.2.1"
actix-rt = "0.2.2" actix-rt = "1.0.0"
actix-web-codegen = { path="actix-web-codegen" } actix-server = "1.0.0"
actix-http = { path = "actix-http", features=["fail"] } actix-testing = "1.0.0"
actix-server = "0.4.1" actix-macros = "0.1.0"
actix-server-config = "0.1.0" actix-threadpool = "0.3.1"
actix-threadpool = "0.1.0" actix-tls = "1.0.0"
awc = { path = "awc", optional = true }
bytes = "0.4" actix-web-codegen = "0.2.0"
derive_more = "0.14" actix-http = "1.0.1"
encoding = "0.2" awc = { version = "1.0.1", default-features = false }
futures = "0.1"
hashbrown = "0.1.8" bytes = "0.5.3"
httparse = "1.3" derive_more = "0.99.2"
encoding_rs = "0.8"
futures = "0.3.1"
fxhash = "0.2.1"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
net2 = "0.2.33" net2 = "0.2.33"
parking_lot = "0.7" pin-project = "0.4.6"
regex = "1.0" regex = "1.3"
serde = { version = "1.0", features=["derive"] } serde = { version = "1.0", features=["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "^0.5.3" serde_urlencoded = "0.6.1"
time = "0.1" time = "0.1.42"
url = { version="1.7", features=["query_encoding"] } url = "2.1"
open-ssl = { version="0.10", package = "openssl", optional = true }
# cookies support rust-tls = { version = "0.16.0", package = "rustls", optional = true }
cookie = { version="0.11", features=["secure", "percent-encode"], optional = true }
# ssl support
native-tls = { version="0.2", optional = true }
openssl = { version="0.10", optional = true }
# rustls = { version = "^0.15", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http = { path = "actix-http", features=["ssl", "brotli", "flate2-zlib"] } actix = "0.9.0"
actix-http-test = { path = "test-server", features=["ssl"] } rand = "0.7"
rand = "0.6"
env_logger = "0.6" env_logger = "0.6"
serde_derive = "1.0" serde_derive = "1.0"
tokio-timer = "0.2.8" brotli2 = "0.3.2"
brotli2 = { version="^0.3.2" } flate2 = "1.0.13"
flate2 = { version="^1.0.2" }
[profile.release] [profile.release]
lto = true lto = true
opt-level = 3 opt-level = 3
codegen-units = 1 codegen-units = 1
[patch.crates-io]
actix-web = { path = "." }
actix-http = { path = "actix-http" }
actix-http-test = { path = "test-server" }
actix-web-codegen = { path = "actix-web-codegen" }
actix-cors = { path = "actix-cors" }
actix-identity = { path = "actix-identity" }
actix-session = { path = "actix-session" }
actix-files = { path = "actix-files" }
actix-multipart = { path = "actix-multipart" }
awc = { path = "awc" }

View File

@ -1,3 +1,364 @@
## 2.0.0
* `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
`.await` on `run` method result, in that case it awaits server exit.
* `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`.
Stored data is available via `HttpRequest::app_data()` method at runtime.
* Extractor configuration must be registered with `App::app_data()` instead of `App::data()`
* Sync handlers has been removed. `.to_async()` method has been renamed to `.to()`
replace `fn` with `async fn` to convert sync handler to async
* `actix_http_test::TestServer` moved to `actix_web::test` module. To start
test server use `test::start()` or `test_start_with_config()` methods
* `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders
http response.
## 1.0.1
* Cors middleware has been moved to `actix-cors` crate
instead of
```rust
use actix_web::middleware::cors::Cors;
```
use
```rust
use actix_cors::Cors;
```
* Identity middleware has been moved to `actix-identity` crate
instead of
```rust
use actix_web::middleware::identity::{Identity, CookieIdentityPolicy, IdentityService};
```
use
```rust
use actix_identity::{Identity, CookieIdentityPolicy, IdentityService};
```
## 1.0.0
* Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration
instead of
```rust
#[derive(Default)]
struct ExtractorConfig {
config: String,
}
impl FromRequest for YourExtractor {
type Config = ExtractorConfig;
type Result = Result<YourExtractor, Error>;
fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result {
println!("use the config: {:?}", cfg.config);
...
}
}
App::new().resource("/route_with_config", |r| {
r.post().with_config(handler_fn, |cfg| {
cfg.0.config = "test".to_string();
})
})
```
use the HttpRequest to get the configuration like any other `Data` with `req.app_data::<C>()` and set it with the `data()` method on the `resource`
```rust
#[derive(Default)]
struct ExtractorConfig {
config: String,
}
impl FromRequest for YourExtractor {
type Error = Error;
type Future = Result<Self, Self::Error>;
type Config = ExtractorConfig;
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let cfg = req.app_data::<ExtractorConfig>();
println!("config data?: {:?}", cfg.unwrap().role);
...
}
}
App::new().service(
resource("/route_with_config")
.data(ExtractorConfig {
config: "test".to_string(),
})
.route(post().to(handler_fn)),
)
```
* Resource registration. 1.0 version uses generalized resource
registration via `.service()` method.
instead of
```rust
App.new().resource("/welcome", |r| r.f(welcome))
```
use App's or Scope's `.service()` method. `.service()` method accepts
object that implements `HttpServiceFactory` trait. By default
actix-web provides `Resource` and `Scope` services.
```rust
App.new().service(
web::resource("/welcome")
.route(web::get().to(welcome))
.route(web::post().to(post_handler))
```
* Scope registration.
instead of
```rust
let app = App::new().scope("/{project_id}", |scope| {
scope
.resource("/path1", |r| r.f(|_| HttpResponse::Ok()))
.resource("/path2", |r| r.f(|_| HttpResponse::Ok()))
.resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed()))
});
```
use `.service()` for registration and `web::scope()` as scope object factory.
```rust
let app = App::new().service(
web::scope("/{project_id}")
.service(web::resource("/path1").to(|| HttpResponse::Ok()))
.service(web::resource("/path2").to(|| HttpResponse::Ok()))
.service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed()))
);
```
* `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`.
instead of
```rust
App.new().resource("/welcome", |r| r.with(welcome))
```
use `.to()` or `.to_async()` methods
```rust
App.new().service(web::resource("/welcome").to(welcome))
```
* Passing arguments to handler with extractors, multiple arguments are allowed
instead of
```rust
fn welcome((body, req): (Bytes, HttpRequest)) -> ... {
...
}
```
use multiple arguments
```rust
fn welcome(body: Bytes, req: HttpRequest) -> ... {
...
}
```
* `.f()`, `.a()` and `.h()` handler registration methods have been removed.
Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
must use extractors.
instead of
```rust
App.new().resource("/welcome", |r| r.f(welcome))
```
use App's `to()` or `to_async()` methods
```rust
App.new().service(web::resource("/welcome").to(welcome))
```
* `HttpRequest` does not provide access to request's payload stream.
instead of
```rust
fn index(req: &HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
req
.payload()
.from_err()
.fold((), |_, chunk| {
...
})
.map(|_| HttpResponse::Ok().finish())
.responder()
}
```
use `Payload` extractor
```rust
fn index(stream: web::Payload) -> impl Future<Item=HttpResponse, Error=Error> {
stream
.from_err()
.fold((), |_, chunk| {
...
})
.map(|_| HttpResponse::Ok().finish())
}
```
* `State` is now `Data`. You register Data during the App initialization process
and then access it from handlers either using a Data extractor or using
HttpRequest's api.
instead of
```rust
App.with_state(T)
```
use App's `data` method
```rust
App.new()
.data(T)
```
and either use the Data extractor within your handler
```rust
use actix_web::web::Data;
fn endpoint_handler(Data<T>)){
...
}
```
.. or access your Data element from the HttpRequest
```rust
fn endpoint_handler(req: HttpRequest) {
let data: Option<Data<T>> = req.app_data::<T>();
}
```
* AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type.
instead of
```rust
use actix_web::AsyncResponder;
fn endpoint_handler(...) -> impl Future<Item=HttpResponse, Error=Error>{
...
.responder()
}
```
.. simply omit AsyncResponder and the corresponding responder() finish method
* Middleware
instead of
```rust
let app = App::new()
.middleware(middleware::Logger::default())
```
use `.wrap()` method
```rust
let app = App::new()
.wrap(middleware::Logger::default())
.route("/index.html", web::get().to(index));
```
* `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
instead of
```rust
fn index(req: &HttpRequest) -> Responder {
req.body()
.and_then(|body| {
...
})
}
```
use
```rust
fn index(body: Bytes) -> Responder {
...
}
```
* `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type
* StaticFiles and NamedFile has been move to separate create.
instead of `use actix_web::fs::StaticFile`
use `use actix_files::Files`
instead of `use actix_web::fs::Namedfile`
use `use actix_files::NamedFile`
* Multipart has been move to separate create.
instead of `use actix_web::multipart::Multipart`
use `use actix_multipart::Multipart`
* Response compression is not enabled by default.
To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
* Session middleware moved to actix-session crate
* Actors support have been moved to `actix-web-actors` crate
* Custom Error
Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller.
Simplest migration from 0.7 to 1.0 shall include below method to the custom implementation of ResponseError:
```rust
fn render_response(&self) -> HttpResponse {
self.error_response()
}
```
## 0.7.15 ## 0.7.15
* The `' '` character is not percent decoded anymore before matching routes. If you need to use it in * The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
@ -38,9 +399,9 @@
* `HttpRequest` does not implement `Stream` anymore. If you need to read request payload * `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
use `HttpMessage::payload()` method. use `HttpMessage::payload()` method.
instead of instead of
```rust ```rust
fn index(req: HttpRequest) -> impl Responder { fn index(req: HttpRequest) -> impl Responder {
req req
@ -66,8 +427,8 @@
trait uses `&HttpRequest` instead of `&mut HttpRequest`. trait uses `&HttpRequest` instead of `&mut HttpRequest`.
* Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead. * Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
instead of instead of
```rust ```rust
fn index(query: Query<..>, info: Json<MyStruct) -> impl Responder {} fn index(query: Query<..>, info: Json<MyStruct) -> impl Responder {}
@ -83,7 +444,7 @@
* `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value * `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
* Removed deprecated `HttpServer::threads()`, use * Removed deprecated `HttpServer::threads()`, use
[HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead. [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
* Renamed `client::ClientConnectorError::Connector` to * Renamed `client::ClientConnectorError::Connector` to
@ -92,7 +453,7 @@
* `Route::with()` does not return `ExtractorConfig`, to configure * `Route::with()` does not return `ExtractorConfig`, to configure
extractor use `Route::with_config()` extractor use `Route::with_config()`
instead of instead of
```rust ```rust
fn main() { fn main() {
@ -103,11 +464,11 @@
}); });
} }
``` ```
use use
```rust ```rust
fn main() { fn main() {
let app = App::new().resource("/index.html", |r| { let app = App::new().resource("/index.html", |r| {
r.method(http::Method::GET) r.method(http::Method::GET)
@ -137,12 +498,12 @@
* `HttpRequest::extensions()` returns read only reference to the request's Extension * `HttpRequest::extensions()` returns read only reference to the request's Extension
`HttpRequest::extensions_mut()` returns mutable reference. `HttpRequest::extensions_mut()` returns mutable reference.
* Instead of * Instead of
`use actix_web::middleware::{ `use actix_web::middleware::{
CookieSessionBackend, CookieSessionError, RequestSession, CookieSessionBackend, CookieSessionError, RequestSession,
Session, SessionBackend, SessionImpl, SessionStorage};` Session, SessionBackend, SessionImpl, SessionStorage};`
use `actix_web::middleware::session` use `actix_web::middleware::session`
`use actix_web::middleware::session{CookieSessionBackend, CookieSessionError, `use actix_web::middleware::session{CookieSessionBackend, CookieSessionError,

View File

@ -1,4 +1,28 @@
# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) <div align="center">
<p><h1>Actix web</h1> </p>
<p><strong>Actix web is a small, pragmatic, and extremely fast rust web framework</strong> </p>
<p>
[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
[![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Documentation](https://docs.rs/actix-web/badge.svg)](https://docs.rs/actix-web)
[![Download](https://img.shields.io/crates/d/actix-web.svg)](https://crates.io/crates/actix-web)
[![Version](https://img.shields.io/badge/rustc-1.39+-lightgray.svg)](https://blog.rust-lang.org/2019/11/07/Rust-1.39.0.html)
![License](https://img.shields.io/crates/l/actix-web.svg)
</p>
<h3>
<a href="https://actix.rs">Website</a>
<span> | </span>
<a href="https://gitter.im/actix/actix">Chat</a>
<span> | </span>
<a href="https://github.com/actix/examples">Examples</a>
</h3>
</div>
<br>
Actix web is a simple, pragmatic and extremely fast web framework for Rust. Actix web is a simple, pragmatic and extremely fast web framework for Rust.
@ -10,37 +34,27 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust.
* Configurable [request routing](https://actix.rs/docs/url-dispatch/) * Configurable [request routing](https://actix.rs/docs/url-dispatch/)
* Multipart streams * Multipart streams
* Static assets * Static assets
* SSL support with OpenSSL or `native-tls` * SSL support with OpenSSL or Rustls
* Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/)) * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) * Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
* Supports [Actix actor framework](https://github.com/actix/actix) * Supports [Actix actor framework](https://github.com/actix/actix)
* Experimental [Async/Await](https://github.com/mehcode/actix-web-async-await) support.
## Documentation & community resources
* [User Guide](https://actix.rs/docs/)
* [API Documentation (Development)](https://actix.rs/actix-web/actix_web/)
* [API Documentation (Releases)](https://docs.rs/actix-web/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-web](https://crates.io/crates/actix-web)
* Minimum supported Rust version: 1.32 or later
## Example ## Example
```rust ```rust
use actix_web::{web, App, HttpServer, Responder}; use actix_web::{get, web, App, HttpServer, Responder};
fn index(info: web::Path<(u32, String)>) -> impl Responder { #[get("/{id}/{name}/index.html")]
async fn index(info: web::Path<(u32, String)>) -> impl Responder {
format!("Hello {}! id:{}", info.1, info.0) format!("Hello {}! id:{}", info.1, info.0)
} }
fn main() -> std::io::Result<()> { #[actix_rt::main]
HttpServer::new( async fn main() -> std::io::Result<()> {
|| App::new() HttpServer::new(|| App::new().service(index))
.service(web::resource("/{id}/{name}/index.html")
.route(web::get().to(index)))
.bind("127.0.0.1:8080")? .bind("127.0.0.1:8080")?
.run(); .run()
.await
} }
``` ```
@ -63,7 +77,7 @@ You may consider checking out
## Benchmarks ## Benchmarks
* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r16&hw=ph&test=plaintext) * [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r18)
## License ## License

15
actix-cors/CHANGES.md Normal file
View File

@ -0,0 +1,15 @@
# Changes
## [0.2.0] - 2019-12-20
* Release
## [0.2.0-alpha.3] - 2019-12-07
* Migrate to actix-web 2.0.0
* Bump `derive_more` crate version to 0.99.0
## [0.1.0] - 2019-06-15
* Move cors middleware to separate crate

26
actix-cors/Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "actix-cors"
version = "0.2.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Cross-origin resource sharing (CORS) for Actix applications."
readme = "README.md"
keywords = ["web", "framework"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-cors/"
license = "MIT/Apache-2.0"
edition = "2018"
workspace = ".."
[lib]
name = "actix_cors"
path = "src/lib.rs"
[dependencies]
actix-web = "2.0.0-rc"
actix-service = "1.0.1"
derive_more = "0.99.2"
futures = "0.3.1"
[dev-dependencies]
actix-rt = "1.0.0"

1
actix-cors/LICENSE-APACHE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-APACHE

1
actix-cors/LICENSE-MIT Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-MIT

9
actix-cors/README.md Normal file
View File

@ -0,0 +1,9 @@
# Cors Middleware for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-cors)](https://crates.io/crates/actix-cors) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & community resources
* [User Guide](https://actix.rs/docs/)
* [API Documentation](https://docs.rs/actix-cors/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-cors](https://crates.io/crates/actix-cors)
* Minimum supported Rust version: 1.34 or later

View File

@ -1,3 +1,4 @@
#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)]
//! Cross-origin resource sharing (CORS) for Actix applications //! Cross-origin resource sharing (CORS) for Actix applications
//! //!
//! CORS middleware could be used with application and with resource. //! CORS middleware could be used with application and with resource.
@ -7,10 +8,10 @@
//! # Example //! # Example
//! //!
//! ```rust //! ```rust
//! use actix_web::middleware::cors::Cors; //! use actix_cors::Cors;
//! use actix_web::{http, web, App, HttpRequest, HttpResponse, HttpServer}; //! use actix_web::{http, web, App, HttpRequest, HttpResponse, HttpServer};
//! //!
//! fn index(req: HttpRequest) -> &'static str { //! async fn index(req: HttpRequest) -> &'static str {
//! "Hello world" //! "Hello world"
//! } //! }
//! //!
@ -22,7 +23,8 @@
//! .allowed_methods(vec!["GET", "POST"]) //! .allowed_methods(vec!["GET", "POST"])
//! .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) //! .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
//! .allowed_header(http::header::CONTENT_TYPE) //! .allowed_header(http::header::CONTENT_TYPE)
//! .max_age(3600)) //! .max_age(3600)
//! .finish())
//! .service( //! .service(
//! web::resource("/index.html") //! web::resource("/index.html")
//! .route(web::get().to(index)) //! .route(web::get().to(index))
@ -38,20 +40,19 @@
//! //!
//! Cors middleware automatically handle *OPTIONS* preflight request. //! Cors middleware automatically handle *OPTIONS* preflight request.
use std::collections::HashSet; use std::collections::HashSet;
use std::convert::TryFrom;
use std::iter::FromIterator; use std::iter::FromIterator;
use std::rc::Rc; use std::rc::Rc;
use std::task::{Context, Poll};
use actix_service::{IntoTransform, Service, Transform}; use actix_service::{Service, Transform};
use actix_web::dev::{RequestHead, ServiceRequest, ServiceResponse};
use actix_web::error::{Error, ResponseError, Result};
use actix_web::http::header::{self, HeaderName, HeaderValue};
use actix_web::http::{self, Error as HttpError, Method, StatusCode, Uri};
use actix_web::HttpResponse;
use derive_more::Display; use derive_more::Display;
use futures::future::{ok, Either, Future, FutureResult}; use futures::future::{ok, Either, FutureExt, LocalBoxFuture, Ready};
use futures::Poll;
use crate::dev::RequestHead;
use crate::error::{ResponseError, Result};
use crate::http::header::{self, HeaderName, HeaderValue};
use crate::http::{self, HttpTryFrom, Method, StatusCode, Uri};
use crate::service::{ServiceRequest, ServiceResponse};
use crate::{HttpMessage, HttpResponse};
/// A set of errors that can occur during processing CORS /// A set of errors that can occur during processing CORS
#[derive(Debug, Display)] #[derive(Debug, Display)]
@ -81,13 +82,6 @@ pub enum CorsError {
fmt = "The request header `Access-Control-Request-Headers` has an invalid value" fmt = "The request header `Access-Control-Request-Headers` has an invalid value"
)] )]
BadRequestHeaders, BadRequestHeaders,
/// The request header `Access-Control-Request-Headers` is required but is
/// missing.
#[display(
fmt = "The request header `Access-Control-Request-Headers` is required but is
missing"
)]
MissingRequestHeaders,
/// Origin is not allowed to make this request /// Origin is not allowed to make this request
#[display(fmt = "Origin is not allowed to make this request")] #[display(fmt = "Origin is not allowed to make this request")]
OriginNotAllowed, OriginNotAllowed,
@ -100,6 +94,10 @@ pub enum CorsError {
} }
impl ResponseError for CorsError { impl ResponseError for CorsError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
HttpResponse::with_body(StatusCode::BAD_REQUEST, format!("{}", self).into()) HttpResponse::with_body(StatusCode::BAD_REQUEST, format!("{}", self).into())
} }
@ -159,11 +157,11 @@ impl<T> AllOrSome<T> {
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// use actix_cors::Cors;
/// use actix_web::http::header; /// use actix_web::http::header;
/// use actix_web::middleware::cors;
/// ///
/// # fn main() { /// # fn main() {
/// let cors = cors::Cors::new() /// let cors = Cors::new()
/// .allowed_origin("https://www.rust-lang.org/") /// .allowed_origin("https://www.rust-lang.org/")
/// .allowed_methods(vec!["GET", "POST"]) /// .allowed_methods(vec!["GET", "POST"])
/// .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) /// .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
@ -171,6 +169,7 @@ impl<T> AllOrSome<T> {
/// .max_age(3600); /// .max_age(3600);
/// # } /// # }
/// ``` /// ```
#[derive(Default)]
pub struct Cors { pub struct Cors {
cors: Option<Inner>, cors: Option<Inner>,
methods: bool, methods: bool,
@ -276,7 +275,8 @@ impl Cors {
pub fn allowed_methods<U, M>(mut self, methods: U) -> Cors pub fn allowed_methods<U, M>(mut self, methods: U) -> Cors
where where
U: IntoIterator<Item = M>, U: IntoIterator<Item = M>,
Method: HttpTryFrom<M>, Method: TryFrom<M>,
<Method as TryFrom<M>>::Error: Into<HttpError>,
{ {
self.methods = true; self.methods = true;
if let Some(cors) = cors(&mut self.cors, &self.error) { if let Some(cors) = cors(&mut self.cors, &self.error) {
@ -298,7 +298,8 @@ impl Cors {
/// Set an allowed header /// Set an allowed header
pub fn allowed_header<H>(mut self, header: H) -> Cors pub fn allowed_header<H>(mut self, header: H) -> Cors
where where
HeaderName: HttpTryFrom<H>, HeaderName: TryFrom<H>,
<HeaderName as TryFrom<H>>::Error: Into<HttpError>,
{ {
if let Some(cors) = cors(&mut self.cors, &self.error) { if let Some(cors) = cors(&mut self.cors, &self.error) {
match HeaderName::try_from(header) { match HeaderName::try_from(header) {
@ -330,7 +331,8 @@ impl Cors {
pub fn allowed_headers<U, H>(mut self, headers: U) -> Cors pub fn allowed_headers<U, H>(mut self, headers: U) -> Cors
where where
U: IntoIterator<Item = H>, U: IntoIterator<Item = H>,
HeaderName: HttpTryFrom<H>, HeaderName: TryFrom<H>,
<HeaderName as TryFrom<H>>::Error: Into<HttpError>,
{ {
if let Some(cors) = cors(&mut self.cors, &self.error) { if let Some(cors) = cors(&mut self.cors, &self.error) {
for h in headers { for h in headers {
@ -364,7 +366,8 @@ impl Cors {
pub fn expose_headers<U, H>(mut self, headers: U) -> Cors pub fn expose_headers<U, H>(mut self, headers: U) -> Cors
where where
U: IntoIterator<Item = H>, U: IntoIterator<Item = H>,
HeaderName: HttpTryFrom<H>, HeaderName: TryFrom<H>,
<HeaderName as TryFrom<H>>::Error: Into<HttpError>,
{ {
for h in headers { for h in headers {
match HeaderName::try_from(h) { match HeaderName::try_from(h) {
@ -463,25 +466,9 @@ impl Cors {
} }
self self
} }
}
fn cors<'a>( /// Construct cors middleware
parts: &'a mut Option<Inner>, pub fn finish(self) -> CorsFactory {
err: &Option<http::Error>,
) -> Option<&'a mut Inner> {
if err.is_some() {
return None;
}
parts.as_mut()
}
impl<S, P, B> IntoTransform<CorsFactory, S> for Cors
where
S: Service<Request = ServiceRequest<P>, Response = ServiceResponse<B>> + 'static,
P: 'static,
B: 'static,
{
fn into_transform(self) -> CorsFactory {
let mut slf = if !self.methods { let mut slf = if !self.methods {
self.allowed_methods(vec![ self.allowed_methods(vec![
Method::GET, Method::GET,
@ -528,6 +515,16 @@ where
} }
} }
fn cors<'a>(
parts: &'a mut Option<Inner>,
err: &Option<http::Error>,
) -> Option<&'a mut Inner> {
if err.is_some() {
return None;
}
parts.as_mut()
}
/// `Middleware` for Cross-origin resource sharing support /// `Middleware` for Cross-origin resource sharing support
/// ///
/// The Cors struct contains the settings for CORS requests to be validated and /// The Cors struct contains the settings for CORS requests to be validated and
@ -536,20 +533,18 @@ pub struct CorsFactory {
inner: Rc<Inner>, inner: Rc<Inner>,
} }
impl<S, P, B> Transform<S> for CorsFactory impl<S, B> Transform<S> for CorsFactory
where where
S: Service<Request = ServiceRequest<P>, Response = ServiceResponse<B>>, S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static, S::Future: 'static,
S::Error: 'static,
P: 'static,
B: 'static, B: 'static,
{ {
type Request = ServiceRequest<P>; type Request = ServiceRequest;
type Response = ServiceResponse<B>; type Response = ServiceResponse<B>;
type Error = S::Error; type Error = Error;
type InitError = (); type InitError = ();
type Transform = CorsMiddleware<S>; type Transform = CorsMiddleware<S>;
type Future = FutureResult<Self::Transform, Self::InitError>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ok(CorsMiddleware { ok(CorsMiddleware {
@ -584,7 +579,7 @@ struct Inner {
impl Inner { impl Inner {
fn validate_origin(&self, req: &RequestHead) -> Result<(), CorsError> { fn validate_origin(&self, req: &RequestHead) -> Result<(), CorsError> {
if let Some(hdr) = req.headers().get(header::ORIGIN) { if let Some(hdr) = req.headers().get(&header::ORIGIN) {
if let Ok(origin) = hdr.to_str() { if let Ok(origin) = hdr.to_str() {
return match self.origins { return match self.origins {
AllOrSome::All => Ok(()), AllOrSome::All => Ok(()),
@ -596,10 +591,10 @@ impl Inner {
} }
Err(CorsError::BadOrigin) Err(CorsError::BadOrigin)
} else { } else {
return match self.origins { match self.origins {
AllOrSome::All => Ok(()), AllOrSome::All => Ok(()),
_ => Err(CorsError::MissingOrigin), _ => Err(CorsError::MissingOrigin),
}; }
} }
} }
@ -608,7 +603,7 @@ impl Inner {
AllOrSome::All => { AllOrSome::All => {
if self.send_wildcard { if self.send_wildcard {
Some(HeaderValue::from_static("*")) Some(HeaderValue::from_static("*"))
} else if let Some(origin) = req.headers().get(header::ORIGIN) { } else if let Some(origin) = req.headers().get(&header::ORIGIN) {
Some(origin.clone()) Some(origin.clone())
} else { } else {
None None
@ -617,7 +612,7 @@ impl Inner {
AllOrSome::Some(ref origins) => { AllOrSome::Some(ref origins) => {
if let Some(origin) = if let Some(origin) =
req.headers() req.headers()
.get(header::ORIGIN) .get(&header::ORIGIN)
.filter(|o| match o.to_str() { .filter(|o| match o.to_str() {
Ok(os) => origins.contains(os), Ok(os) => origins.contains(os),
_ => false, _ => false,
@ -632,7 +627,7 @@ impl Inner {
} }
fn validate_allowed_method(&self, req: &RequestHead) -> Result<(), CorsError> { fn validate_allowed_method(&self, req: &RequestHead) -> Result<(), CorsError> {
if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) { if let Some(hdr) = req.headers().get(&header::ACCESS_CONTROL_REQUEST_METHOD) {
if let Ok(meth) = hdr.to_str() { if let Ok(meth) = hdr.to_str() {
if let Ok(method) = Method::try_from(meth) { if let Ok(method) = Method::try_from(meth) {
return self return self
@ -653,7 +648,7 @@ impl Inner {
AllOrSome::All => Ok(()), AllOrSome::All => Ok(()),
AllOrSome::Some(ref allowed_headers) => { AllOrSome::Some(ref allowed_headers) => {
if let Some(hdr) = if let Some(hdr) =
req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) req.headers().get(&header::ACCESS_CONTROL_REQUEST_HEADERS)
{ {
if let Ok(headers) = hdr.to_str() { if let Ok(headers) = hdr.to_str() {
let mut hdrs = HashSet::new(); let mut hdrs = HashSet::new();
@ -663,50 +658,51 @@ impl Inner {
Err(_) => return Err(CorsError::BadRequestHeaders), Err(_) => return Err(CorsError::BadRequestHeaders),
}; };
} }
// `Access-Control-Request-Headers` must contain 1 or more
if !hdrs.is_empty() && !hdrs.is_subset(allowed_headers) { // `field-name`.
return Err(CorsError::HeadersNotAllowed); if !hdrs.is_empty() {
if !hdrs.is_subset(allowed_headers) {
return Err(CorsError::HeadersNotAllowed);
}
return Ok(());
} }
return Ok(());
} }
Err(CorsError::BadRequestHeaders) Err(CorsError::BadRequestHeaders)
} else { } else {
Err(CorsError::MissingRequestHeaders) Ok(())
} }
} }
} }
} }
} }
impl<S, P, B> Service for CorsMiddleware<S> impl<S, B> Service for CorsMiddleware<S>
where where
S: Service<Request = ServiceRequest<P>, Response = ServiceResponse<B>>, S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static, S::Future: 'static,
S::Error: 'static,
P: 'static,
B: 'static, B: 'static,
{ {
type Request = ServiceRequest<P>; type Request = ServiceRequest;
type Response = ServiceResponse<B>; type Response = ServiceResponse<B>;
type Error = S::Error; type Error = Error;
type Future = Either< type Future = Either<
FutureResult<Self::Response, Self::Error>, Ready<Result<Self::Response, Error>>,
Either<S::Future, Box<Future<Item = Self::Response, Error = Self::Error>>>, LocalBoxFuture<'static, Result<Self::Response, Error>>,
>; >;
fn poll_ready(&mut self) -> Poll<(), Self::Error> { fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready() self.service.poll_ready(cx)
} }
fn call(&mut self, req: ServiceRequest<P>) -> Self::Future { fn call(&mut self, req: ServiceRequest) -> Self::Future {
if self.inner.preflight && Method::OPTIONS == *req.method() { if self.inner.preflight && Method::OPTIONS == *req.method() {
if let Err(e) = self if let Err(e) = self
.inner .inner
.validate_origin(&req) .validate_origin(req.head())
.and_then(|_| self.inner.validate_allowed_method(&req)) .and_then(|_| self.inner.validate_allowed_method(req.head()))
.and_then(|_| self.inner.validate_allowed_headers(&req)) .and_then(|_| self.inner.validate_allowed_headers(req.head()))
{ {
return Either::A(ok(req.error_response(e))); return Either::Left(ok(req.error_response(e)));
} }
// allowed headers // allowed headers
@ -721,7 +717,7 @@ where
.unwrap(), .unwrap(),
) )
} else if let Some(hdr) = } else if let Some(hdr) =
req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) req.headers().get(&header::ACCESS_CONTROL_REQUEST_HEADERS)
{ {
Some(hdr.clone()) Some(hdr.clone())
} else { } else {
@ -739,7 +735,7 @@ where
let _ = resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); let _ = resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers);
}) })
.if_some( .if_some(
self.inner.access_control_allow_origin(&req), self.inner.access_control_allow_origin(req.head()),
|origin, resp| { |origin, resp| {
let _ = resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin); let _ = resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin);
}, },
@ -759,39 +755,50 @@ where
.finish() .finish()
.into_body(); .into_body();
Either::A(ok(req.into_response(res))) Either::Left(ok(req.into_response(res)))
} else if req.headers().contains_key(header::ORIGIN) { } else {
// Only check requests with a origin header. if req.headers().contains_key(&header::ORIGIN) {
if let Err(e) = self.inner.validate_origin(&req) { // Only check requests with a origin header.
return Either::A(ok(req.error_response(e))); if let Err(e) = self.inner.validate_origin(req.head()) {
return Either::Left(ok(req.error_response(e)));
}
} }
let inner = self.inner.clone(); let inner = self.inner.clone();
let has_origin = req.headers().contains_key(&header::ORIGIN);
let fut = self.service.call(req);
Either::B(Either::B(Box::new(self.service.call(req).and_then( Either::Right(
move |mut res| { async move {
if let Some(origin) = let res = fut.await;
inner.access_control_allow_origin(&res.request())
{
res.headers_mut()
.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone());
};
if let Some(ref expose) = inner.expose_hdrs { if has_origin {
res.headers_mut().insert( let mut res = res?;
header::ACCESS_CONTROL_EXPOSE_HEADERS, if let Some(origin) =
HeaderValue::try_from(expose.as_str()).unwrap(), inner.access_control_allow_origin(res.request().head())
); {
} res.headers_mut().insert(
if inner.supports_credentials { header::ACCESS_CONTROL_ALLOW_ORIGIN,
res.headers_mut().insert( origin.clone(),
header::ACCESS_CONTROL_ALLOW_CREDENTIALS, );
HeaderValue::from_static("true"), };
);
} if let Some(ref expose) = inner.expose_hdrs {
if inner.vary_header { res.headers_mut().insert(
let value = header::ACCESS_CONTROL_EXPOSE_HEADERS,
if let Some(hdr) = res.headers_mut().get(header::VARY) { HeaderValue::try_from(expose.as_str()).unwrap(),
);
}
if inner.supports_credentials {
res.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
HeaderValue::from_static("true"),
);
}
if inner.vary_header {
let value = if let Some(hdr) =
res.headers_mut().get(&header::VARY)
{
let mut val: Vec<u8> = let mut val: Vec<u8> =
Vec::with_capacity(hdr.as_bytes().len() + 8); Vec::with_capacity(hdr.as_bytes().len() + 8);
val.extend(hdr.as_bytes()); val.extend(hdr.as_bytes());
@ -800,87 +807,89 @@ where
} else { } else {
HeaderValue::from_static("Origin") HeaderValue::from_static("Origin")
}; };
res.headers_mut().insert(header::VARY, value); res.headers_mut().insert(header::VARY, value);
}
Ok(res)
} else {
res
} }
Ok(res) }
}, .boxed_local(),
)))) )
} else {
Either::B(Either::A(self.service.call(req)))
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_service::{FnService, Transform}; use actix_service::{fn_service, Transform};
use actix_web::test::{self, TestRequest};
use super::*; use super::*;
use crate::dev::PayloadStream;
use crate::test::{self, block_on, TestRequest};
impl Cors { #[actix_rt::test]
fn finish<S, P, B>(self, srv: S) -> CorsMiddleware<S>
where
S: Service<Request = ServiceRequest<P>, Response = ServiceResponse<B>>
+ 'static,
S::Future: 'static,
S::Error: 'static,
P: 'static,
B: 'static,
{
block_on(
IntoTransform::<CorsFactory, S>::into_transform(self).new_transform(srv),
)
.unwrap()
}
}
#[test]
#[should_panic(expected = "Credentials are allowed, but the Origin is set to")] #[should_panic(expected = "Credentials are allowed, but the Origin is set to")]
fn cors_validates_illegal_allow_credentials() { async fn cors_validates_illegal_allow_credentials() {
let _cors = Cors::new() let _cors = Cors::new().supports_credentials().send_wildcard().finish();
.supports_credentials()
.send_wildcard()
.finish(test::ok_service());
} }
#[test] #[actix_rt::test]
fn validate_origin_allows_all_origins() { async fn validate_origin_allows_all_origins() {
let mut cors = Cors::new().finish(test::ok_service()); let mut cors = Cors::new()
let req = .finish()
TestRequest::with_header("Origin", "https://www.example.com").to_service(); .new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://www.example.com")
.to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[test] #[actix_rt::test]
fn test_preflight() { async fn default() {
let mut cors = Cors::default()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://www.example.com")
.to_srv_request();
let resp = test::call_service(&mut cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_preflight() {
let mut cors = Cors::new() let mut cors = Cors::new()
.send_wildcard() .send_wildcard()
.max_age(3600) .max_age(3600)
.allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST]) .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST])
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
.allowed_header(header::CONTENT_TYPE) .allowed_header(header::CONTENT_TYPE)
.finish(test::ok_service()); .finish()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://www.example.com") let req = TestRequest::with_header("Origin", "https://www.example.com")
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_service(); .header(header::ACCESS_CONTROL_REQUEST_HEADERS, "X-Not-Allowed")
.to_srv_request();
assert!(cors.inner.validate_allowed_method(&req).is_err()); assert!(cors.inner.validate_allowed_method(req.head()).is_err());
assert!(cors.inner.validate_allowed_headers(&req).is_err()); assert!(cors.inner.validate_allowed_headers(req.head()).is_err());
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::with_header("Origin", "https://www.example.com") let req = TestRequest::with_header("Origin", "https://www.example.com")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "put") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "put")
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_service(); .to_srv_request();
assert!(cors.inner.validate_allowed_method(&req).is_err()); assert!(cors.inner.validate_allowed_method(req.head()).is_err());
assert!(cors.inner.validate_allowed_headers(&req).is_err()); assert!(cors.inner.validate_allowed_headers(req.head()).is_ok());
let req = TestRequest::with_header("Origin", "https://www.example.com") let req = TestRequest::with_header("Origin", "https://www.example.com")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
@ -889,26 +898,26 @@ mod tests {
"AUTHORIZATION,ACCEPT", "AUTHORIZATION,ACCEPT",
) )
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!( assert_eq!(
&b"*"[..], &b"*"[..],
resp.headers() resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN) .get(&header::ACCESS_CONTROL_ALLOW_ORIGIN)
.unwrap() .unwrap()
.as_bytes() .as_bytes()
); );
assert_eq!( assert_eq!(
&b"3600"[..], &b"3600"[..],
resp.headers() resp.headers()
.get(header::ACCESS_CONTROL_MAX_AGE) .get(&header::ACCESS_CONTROL_MAX_AGE)
.unwrap() .unwrap()
.as_bytes() .as_bytes()
); );
let hdr = resp let hdr = resp
.headers() .headers()
.get(header::ACCESS_CONTROL_ALLOW_HEADERS) .get(&header::ACCESS_CONTROL_ALLOW_HEADERS)
.unwrap() .unwrap()
.to_str() .to_str()
.unwrap(); .unwrap();
@ -935,15 +944,15 @@ mod tests {
"AUTHORIZATION,ACCEPT", "AUTHORIZATION,ACCEPT",
) )
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
// #[test] // #[actix_rt::test]
// #[should_panic(expected = "MissingOrigin")] // #[should_panic(expected = "MissingOrigin")]
// fn test_validate_missing_origin() { // async fn test_validate_missing_origin() {
// let cors = Cors::build() // let cors = Cors::build()
// .allowed_origin("https://www.example.com") // .allowed_origin("https://www.example.com")
// .finish(); // .finish();
@ -951,41 +960,52 @@ mod tests {
// cors.start(&req).unwrap(); // cors.start(&req).unwrap();
// } // }
#[test] #[actix_rt::test]
#[should_panic(expected = "OriginNotAllowed")] #[should_panic(expected = "OriginNotAllowed")]
fn test_validate_not_allowed_origin() { async fn test_validate_not_allowed_origin() {
let cors = Cors::new() let cors = Cors::new()
.allowed_origin("https://www.example.com") .allowed_origin("https://www.example.com")
.finish(test::ok_service()); .finish()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://www.unknown.com") let req = TestRequest::with_header("Origin", "https://www.unknown.com")
.method(Method::GET) .method(Method::GET)
.to_service(); .to_srv_request();
cors.inner.validate_origin(&req).unwrap(); cors.inner.validate_origin(req.head()).unwrap();
cors.inner.validate_allowed_method(&req).unwrap(); cors.inner.validate_allowed_method(req.head()).unwrap();
cors.inner.validate_allowed_headers(&req).unwrap(); cors.inner.validate_allowed_headers(req.head()).unwrap();
} }
#[test] #[actix_rt::test]
fn test_validate_origin() { async fn test_validate_origin() {
let mut cors = Cors::new() let mut cors = Cors::new()
.allowed_origin("https://www.example.com") .allowed_origin("https://www.example.com")
.finish(test::ok_service()); .finish()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://www.example.com") let req = TestRequest::with_header("Origin", "https://www.example.com")
.method(Method::GET) .method(Method::GET)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[test] #[actix_rt::test]
fn test_no_origin_response() { async fn test_no_origin_response() {
let mut cors = Cors::new().disable_preflight().finish(test::ok_service()); let mut cors = Cors::new()
.disable_preflight()
.finish()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default().method(Method::GET).to_service(); let req = TestRequest::default().method(Method::GET).to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert!(resp assert!(resp
.headers() .headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN) .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
@ -993,8 +1013,8 @@ mod tests {
let req = TestRequest::with_header("Origin", "https://www.example.com") let req = TestRequest::with_header("Origin", "https://www.example.com")
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!( assert_eq!(
&b"https://www.example.com"[..], &b"https://www.example.com"[..],
resp.headers() resp.headers()
@ -1004,8 +1024,8 @@ mod tests {
); );
} }
#[test] #[actix_rt::test]
fn test_response() { async fn test_response() {
let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT]; let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT];
let mut cors = Cors::new() let mut cors = Cors::new()
.send_wildcard() .send_wildcard()
@ -1015,13 +1035,16 @@ mod tests {
.allowed_headers(exposed_headers.clone()) .allowed_headers(exposed_headers.clone())
.expose_headers(exposed_headers.clone()) .expose_headers(exposed_headers.clone())
.allowed_header(header::CONTENT_TYPE) .allowed_header(header::CONTENT_TYPE)
.finish(test::ok_service()); .finish()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://www.example.com") let req = TestRequest::with_header("Origin", "https://www.example.com")
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!( assert_eq!(
&b"*"[..], &b"*"[..],
resp.headers() resp.headers()
@ -1059,15 +1082,18 @@ mod tests {
.allowed_headers(exposed_headers.clone()) .allowed_headers(exposed_headers.clone())
.expose_headers(exposed_headers.clone()) .expose_headers(exposed_headers.clone())
.allowed_header(header::CONTENT_TYPE) .allowed_header(header::CONTENT_TYPE)
.finish(FnService::new(move |req: ServiceRequest<PayloadStream>| { .finish()
req.into_response( .new_transform(fn_service(|req: ServiceRequest| {
ok(req.into_response(
HttpResponse::Ok().header(header::VARY, "Accept").finish(), HttpResponse::Ok().header(header::VARY, "Accept").finish(),
) ))
})); }))
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://www.example.com") let req = TestRequest::with_header("Origin", "https://www.example.com")
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!( assert_eq!(
&b"Accept, Origin"[..], &b"Accept, Origin"[..],
resp.headers().get(header::VARY).unwrap().as_bytes() resp.headers().get(header::VARY).unwrap().as_bytes()
@ -1077,13 +1103,16 @@ mod tests {
.disable_vary_header() .disable_vary_header()
.allowed_origin("https://www.example.com") .allowed_origin("https://www.example.com")
.allowed_origin("https://www.google.com") .allowed_origin("https://www.google.com")
.finish(test::ok_service()); .finish()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://www.example.com") let req = TestRequest::with_header("Origin", "https://www.example.com")
.method(Method::OPTIONS) .method(Method::OPTIONS)
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
let origins_str = resp let origins_str = resp
.headers() .headers()
@ -1095,19 +1124,22 @@ mod tests {
assert_eq!("https://www.example.com", origins_str); assert_eq!("https://www.example.com", origins_str);
} }
#[test] #[actix_rt::test]
fn test_multiple_origins() { async fn test_multiple_origins() {
let mut cors = Cors::new() let mut cors = Cors::new()
.allowed_origin("https://example.com") .allowed_origin("https://example.com")
.allowed_origin("https://example.org") .allowed_origin("https://example.org")
.allowed_methods(vec![Method::GET]) .allowed_methods(vec![Method::GET])
.finish(test::ok_service()); .finish()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://example.com") let req = TestRequest::with_header("Origin", "https://example.com")
.method(Method::GET) .method(Method::GET)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!( assert_eq!(
&b"https://example.com"[..], &b"https://example.com"[..],
resp.headers() resp.headers()
@ -1118,9 +1150,9 @@ mod tests {
let req = TestRequest::with_header("Origin", "https://example.org") let req = TestRequest::with_header("Origin", "https://example.org")
.method(Method::GET) .method(Method::GET)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!( assert_eq!(
&b"https://example.org"[..], &b"https://example.org"[..],
resp.headers() resp.headers()
@ -1130,20 +1162,23 @@ mod tests {
); );
} }
#[test] #[actix_rt::test]
fn test_multiple_origins_preflight() { async fn test_multiple_origins_preflight() {
let mut cors = Cors::new() let mut cors = Cors::new()
.allowed_origin("https://example.com") .allowed_origin("https://example.com")
.allowed_origin("https://example.org") .allowed_origin("https://example.org")
.allowed_methods(vec![Method::GET]) .allowed_methods(vec![Method::GET])
.finish(test::ok_service()); .finish()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_header("Origin", "https://example.com") let req = TestRequest::with_header("Origin", "https://example.com")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!( assert_eq!(
&b"https://example.com"[..], &b"https://example.com"[..],
resp.headers() resp.headers()
@ -1155,9 +1190,9 @@ mod tests {
let req = TestRequest::with_header("Origin", "https://example.org") let req = TestRequest::with_header("Origin", "https://example.org")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_service(); .to_srv_request();
let resp = test::call_success(&mut cors, req); let resp = test::call_service(&mut cors, req).await;
assert_eq!( assert_eq!(
&b"https://example.org"[..], &b"https://example.org"[..],
resp.headers() resp.headers()

View File

@ -1,5 +1,76 @@
# Changes # Changes
## [0.1.0] - 2018-10-x ## [0.2.1] - 2019-12-22
* Use the same format for file URLs regardless of platforms
## [0.2.0] - 2019-12-20
* Fix BodyEncoding trait import #1220
## [0.2.0-alpha.1] - 2019-12-07
* Migrate to `std::future`
## [0.1.7] - 2019-11-06
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
## [0.1.6] - 2019-10-14
* Add option to redirect to a slash-ended path `Files` #1132
## [0.1.5] - 2019-10-08
* Bump up `mime_guess` crate version to 2.0.1
* Bump up `percent-encoding` crate version to 2.1
* Allow user defined request guards for `Files` #1113
## [0.1.4] - 2019-07-20
* Allow to disable `Content-Disposition` header #686
## [0.1.3] - 2019-06-28
* Do not set `Content-Length` header, let actix-http set it #930
## [0.1.2] - 2019-06-13
* Content-Length is 0 for NamedFile HEAD request #914
* Fix ring dependency from actix-web default features for #741
## [0.1.1] - 2019-06-01
* Static files are incorrectly served as both chunked and with length #812
## [0.1.0] - 2019-05-25
* NamedFile last-modified check always fails due to nano-seconds
in file modified date #820
## [0.1.0-beta.4] - 2019-05-12
* Update actix-web to beta.4
## [0.1.0-beta.1] - 2019-04-20
* Update actix-web to beta.1
## [0.1.0-alpha.6] - 2019-04-14
* Update actix-web to alpha6
## [0.1.0-alpha.4] - 2019-04-08
* Update actix-web to alpha4
## [0.1.0-alpha.2] - 2019-04-02
* Add default handler support
## [0.1.0-alpha.1] - 2019-03-28
* Initial impl * Initial impl

View File

@ -1,8 +1,8 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.1.0-alpha.1" version = "0.2.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static files support for Actix web." description = "Static files support for actix web."
readme = "README.md" readme = "README.md"
keywords = ["actix", "http", "async", "futures"] keywords = ["actix", "http", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -18,19 +18,19 @@ name = "actix_files"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { path=".." } actix-web = { version = "2.0.0-rc", default-features = false }
actix-http = { path="../actix-http" } actix-http = "1.0.1"
actix-service = "0.3.3" actix-service = "1.0.1"
bitflags = "1" bitflags = "1"
bytes = "0.4" bytes = "0.5.3"
futures = "0.1" futures = "0.3.1"
derive_more = "0.14" derive_more = "0.99.2"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
mime_guess = "2.0.0-alpha" mime_guess = "2.0.1"
percent-encoding = "1.0" percent-encoding = "2.1"
v_htmlescape = "0.4" v_htmlescape = "0.4"
[dev-dependencies] [dev-dependencies]
actix-web = { path="..", features=["ssl"] } actix-rt = "1.0.0"
actix-web = { version = "2.0.0-rc", features=["openssl"] }

1
actix-files/LICENSE-APACHE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-APACHE

1
actix-files/LICENSE-MIT Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-MIT

View File

@ -1,82 +1,9 @@
# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # Static files support for actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-files)](https://crates.io/crates/actix-files) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Actix web is a simple, pragmatic and extremely fast web framework for Rust.
* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.rs/docs/http2/) protocols
* Streaming and pipelining
* Keep-alive and slow requests handling
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
* Transparent content compression/decompression (br, gzip, deflate)
* Configurable [request routing](https://actix.rs/docs/url-dispatch/)
* Multipart streams
* Static assets
* SSL support with OpenSSL or `native-tls`
* Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/))
* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
* Built on top of [Actix actor framework](https://github.com/actix/actix)
* Experimental [Async/Await](https://github.com/mehcode/actix-web-async-await) support.
## Documentation & community resources ## Documentation & community resources
* [User Guide](https://actix.rs/docs/) * [User Guide](https://actix.rs/docs/)
* [API Documentation (Development)](https://actix.rs/actix-web/actix_web/) * [API Documentation](https://docs.rs/actix-files/)
* [API Documentation (Releases)](https://actix.rs/api/actix-web/stable/actix_web/)
* [Chat on gitter](https://gitter.im/actix/actix) * [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-web](https://crates.io/crates/actix-web) * Cargo package: [actix-files](https://crates.io/crates/actix-files)
* Minimum supported Rust version: 1.31 or later * Minimum supported Rust version: 1.33 or later
## Example
```rust
extern crate actix_web;
use actix_web::{http, server, App, Path, Responder};
fn index(info: Path<(u32, String)>) -> impl Responder {
format!("Hello {}! id:{}", info.1, info.0)
}
fn main() {
server::new(
|| App::new()
.route("/{id}/{name}/index.html", http::Method::GET, index))
.bind("127.0.0.1:8080").unwrap()
.run();
}
```
### More examples
* [Basics](https://github.com/actix/examples/tree/master/basics/)
* [Stateful](https://github.com/actix/examples/tree/master/state/)
* [Protobuf support](https://github.com/actix/examples/tree/master/protobuf/)
* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/)
* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/)
* [Tera](https://github.com/actix/examples/tree/master/template_tera/) /
[Askama](https://github.com/actix/examples/tree/master/template_askama/) templates
* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/)
* [r2d2](https://github.com/actix/examples/tree/master/r2d2/)
* [SSL / HTTP/2.0](https://github.com/actix/examples/tree/master/tls/)
* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/)
* [Json](https://github.com/actix/examples/tree/master/json/)
You may consider checking out
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
## Benchmarks
* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r16&hw=ph&test=plaintext)
## License
This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
at your option.
## Code of Conduct
Contribution to the actix-web crate is organized under the terms of the
Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to
intervene to uphold that code of conduct.

View File

@ -35,7 +35,7 @@ pub enum UriSegmentError {
/// Return `BadRequest` for `UriSegmentError` /// Return `BadRequest` for `UriSegmentError`
impl ResponseError for UriSegmentError { impl ResponseError for UriSegmentError {
fn error_response(&self) -> HttpResponse { fn status_code(&self) -> StatusCode {
HttpResponse::new(StatusCode::BAD_REQUEST) StatusCode::BAD_REQUEST
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,22 +9,25 @@ use std::os::unix::fs::MetadataExt;
use bitflags::bitflags; use bitflags::bitflags;
use mime; use mime;
use mime_guess::guess_mime_type; use mime_guess::from_path;
use actix_http::body::SizedStream;
use actix_web::dev::BodyEncoding;
use actix_web::http::header::{ use actix_web::http::header::{
self, ContentDisposition, DispositionParam, DispositionType, self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
}; };
use actix_web::http::{ContentEncoding, Method, StatusCode}; use actix_web::http::{ContentEncoding, StatusCode};
use actix_web::middleware::encoding::BodyEncoding;
use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder};
use futures::future::{ready, Ready};
use crate::range::HttpRange; use crate::range::HttpRange;
use crate::ChunkedReadFile; use crate::ChunkedReadFile;
bitflags! { bitflags! {
pub(crate) struct Flags: u32 { pub(crate) struct Flags: u8 {
const ETAG = 0b00000001; const ETAG = 0b0000_0001;
const LAST_MD = 0b00000010; const LAST_MD = 0b0000_0010;
const CONTENT_DISPOSITION = 0b0000_0100;
} }
} }
@ -39,13 +42,13 @@ impl Default for Flags {
pub struct NamedFile { pub struct NamedFile {
path: PathBuf, path: PathBuf,
file: File, file: File,
modified: Option<SystemTime>,
pub(crate) md: Metadata,
pub(crate) flags: Flags,
pub(crate) status_code: StatusCode,
pub(crate) content_type: mime::Mime, pub(crate) content_type: mime::Mime,
pub(crate) content_disposition: header::ContentDisposition, pub(crate) content_disposition: header::ContentDisposition,
pub(crate) md: Metadata, pub(crate) encoding: Option<ContentEncoding>,
modified: Option<SystemTime>,
encoding: Option<ContentEncoding>,
pub(crate) status_code: StatusCode,
pub(crate) flags: Flags,
} }
impl NamedFile { impl NamedFile {
@ -66,6 +69,7 @@ impl NamedFile {
/// let mut file = File::create("foo.txt")?; /// let mut file = File::create("foo.txt")?;
/// file.write_all(b"Hello, world!")?; /// file.write_all(b"Hello, world!")?;
/// let named_file = NamedFile::from_file(file, "bar.txt")?; /// let named_file = NamedFile::from_file(file, "bar.txt")?;
/// # std::fs::remove_file("foo.txt");
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
@ -85,14 +89,23 @@ impl NamedFile {
} }
}; };
let ct = guess_mime_type(&path); let ct = from_path(&path).first_or_octet_stream();
let disposition_type = match ct.type_() { let disposition_type = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
let mut parameters =
vec![DispositionParam::Filename(String::from(filename.as_ref()))];
if !filename.is_ascii() {
parameters.push(DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Ext(String::from("UTF-8")),
language_tag: None,
value: filename.into_owned().into_bytes(),
}))
}
let cd = ContentDisposition { let cd = ContentDisposition {
disposition: disposition_type, disposition: disposition_type,
parameters: vec![DispositionParam::Filename(filename.into_owned())], parameters: parameters,
}; };
(ct, cd) (ct, cd)
}; };
@ -170,11 +183,21 @@ impl NamedFile {
/// sent to the peer. By default the disposition is `inline` for text, /// sent to the peer. By default the disposition is `inline` for text,
/// image, and video content types, and `attachment` otherwise, and /// image, and video content types, and `attachment` otherwise, and
/// the filename is taken from the path provided in the `open` method /// the filename is taken from the path provided in the `open` method
/// after converting it to UTF-8 using /// after converting it to UTF-8 using.
/// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy). /// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy).
#[inline] #[inline]
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
self.content_disposition = cd; self.content_disposition = cd;
self.flags.insert(Flags::CONTENT_DISPOSITION);
self
}
/// Disable `Content-Disposition` header.
///
/// By default Content-Disposition` header is enabled.
#[inline]
pub fn disable_content_disposition(mut self) -> Self {
self.flags.remove(Flags::CONTENT_DISPOSITION);
self self
} }
@ -233,6 +256,144 @@ impl NamedFile {
pub(crate) fn last_modified(&self) -> Option<header::HttpDate> { pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
self.modified.map(|mtime| mtime.into()) self.modified.map(|mtime| mtime.into())
} }
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
if self.status_code != StatusCode::OK {
let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone()))
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
res.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
});
if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding);
}
let reader = ChunkedReadFile {
size: self.md.len(),
offset: 0,
file: Some(self.file),
fut: None,
counter: 0,
};
return Ok(resp.streaming(reader));
}
let etag = if self.flags.contains(Flags::ETAG) {
self.etag()
} else {
None
};
let last_modified = if self.flags.contains(Flags::LAST_MD) {
self.last_modified()
} else {
None
};
// check preconditions
let precondition_failed = if !any_match(etag.as_ref(), req) {
true
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
(last_modified, req.get_header())
{
let t1: SystemTime = m.clone().into();
let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1 > t2,
_ => false,
}
} else {
false
};
// check last modified
let not_modified = if !none_match(etag.as_ref(), req) {
true
} else if req.headers().contains_key(&header::IF_NONE_MATCH) {
false
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
(last_modified, req.get_header())
{
let t1: SystemTime = m.clone().into();
let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1 <= t2,
_ => false,
}
} else {
false
};
let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone()))
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
res.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
});
// default compressing
if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding);
}
resp.if_some(last_modified, |lm, resp| {
resp.set(header::LastModified(lm));
})
.if_some(etag, |etag, resp| {
resp.set(header::ETag(etag));
});
resp.header(header::ACCEPT_RANGES, "bytes");
let mut length = self.md.len();
let mut offset = 0;
// check for range header
if let Some(ranges) = req.headers().get(&header::RANGE) {
if let Ok(rangesheader) = ranges.to_str() {
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
length = rangesvec[0].length;
offset = rangesvec[0].start;
resp.encoding(ContentEncoding::Identity);
resp.header(
header::CONTENT_RANGE,
format!(
"bytes {}-{}/{}",
offset,
offset + length - 1,
self.md.len()
),
);
} else {
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
};
} else {
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
};
};
if precondition_failed {
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
} else if not_modified {
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
}
let reader = ChunkedReadFile {
offset,
size: length,
file: Some(self.file),
fut: None,
counter: 0,
};
if offset != 0 || length != self.md.len() {
Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader))
} else {
Ok(resp.body(SizedStream::new(length, reader)))
}
}
} }
impl Deref for NamedFile { impl Deref for NamedFile {
@ -286,145 +447,9 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
impl Responder for NamedFile { impl Responder for NamedFile {
type Error = Error; type Error = Error;
type Future = Result<HttpResponse, Error>; type Future = Ready<Result<HttpResponse, Error>>;
fn respond_to(self, req: &HttpRequest) -> Self::Future { fn respond_to(self, req: &HttpRequest) -> Self::Future {
if self.status_code != StatusCode::OK { ready(self.into_response(req))
let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone()))
.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
// TODO blocking by compressing
// if let Some(current_encoding) = self.encoding {
// resp.content_encoding(current_encoding);
// }
let reader = ChunkedReadFile {
size: self.md.len(),
offset: 0,
file: Some(self.file),
fut: None,
counter: 0,
};
return Ok(resp.streaming(reader));
}
match req.method() {
&Method::HEAD | &Method::GET => (),
_ => {
return Ok(HttpResponse::MethodNotAllowed()
.header(header::CONTENT_TYPE, "text/plain")
.header(header::ALLOW, "GET, HEAD")
.body("This resource only supports GET and HEAD."));
}
}
let etag = if self.flags.contains(Flags::ETAG) {
self.etag()
} else {
None
};
let last_modified = if self.flags.contains(Flags::LAST_MD) {
self.last_modified()
} else {
None
};
// check preconditions
let precondition_failed = if !any_match(etag.as_ref(), req) {
true
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
(last_modified, req.get_header())
{
m > since
} else {
false
};
// check last modified
let not_modified = if !none_match(etag.as_ref(), req) {
true
} else if req.headers().contains_key(header::IF_NONE_MATCH) {
false
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
(last_modified, req.get_header())
{
m <= since
} else {
false
};
let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone()))
.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
// default compressing
if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding);
}
resp.if_some(last_modified, |lm, resp| {
resp.set(header::LastModified(lm));
})
.if_some(etag, |etag, resp| {
resp.set(header::ETag(etag));
});
resp.header(header::ACCEPT_RANGES, "bytes");
let mut length = self.md.len();
let mut offset = 0;
// check for range header
if let Some(ranges) = req.headers().get(header::RANGE) {
if let Ok(rangesheader) = ranges.to_str() {
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
length = rangesvec[0].length;
offset = rangesvec[0].start;
resp.encoding(ContentEncoding::Identity);
resp.header(
header::CONTENT_RANGE,
format!(
"bytes {}-{}/{}",
offset,
offset + length - 1,
self.md.len()
),
);
} else {
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
};
} else {
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
};
};
resp.header(header::CONTENT_LENGTH, format!("{}", length));
if precondition_failed {
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
} else if not_modified {
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
}
if *req.method() == Method::HEAD {
Ok(resp.finish())
} else {
let reader = ChunkedReadFile {
offset,
size: length,
file: Some(self.file),
fut: None,
counter: 0,
};
if offset != 0 || length != self.md.len() {
return Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader));
};
Ok(resp.streaming(reader))
}
} }
} }

View File

@ -5,7 +5,7 @@ pub struct HttpRange {
pub length: u64, pub length: u64,
} }
static PREFIX: &'static str = "bytes="; static PREFIX: &str = "bytes=";
const PREFIX_LEN: usize = 6; const PREFIX_LEN: usize = 6;
impl HttpRange { impl HttpRange {

37
actix-framed/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "actix-framed"
version = "0.3.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix framed app server"
readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-framed/"
categories = ["network-programming", "asynchronous",
"web-programming::http-server",
"web-programming::websocket"]
license = "MIT/Apache-2.0"
edition = "2018"
[lib]
name = "actix_framed"
path = "src/lib.rs"
[dependencies]
actix-codec = "0.2.0"
actix-service = "1.0.1"
actix-router = "0.2.1"
actix-rt = "1.0.0"
actix-http = "1.0.1"
bytes = "0.5.3"
futures = "0.3.1"
pin-project = "0.4.6"
log = "0.4"
[dev-dependencies]
actix-server = "1.0.0"
actix-connect = { version = "1.0.0", features=["openssl"] }
actix-http-test = { version = "1.0.0", features=["openssl"] }
actix-utils = "1.0.3"

201
actix-framed/LICENSE-APACHE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017-NOW Nikolay Kim
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

25
actix-framed/LICENSE-MIT Normal file
View File

@ -0,0 +1,25 @@
Copyright (c) 2017 Nikolay Kim
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

8
actix-framed/README.md Normal file
View File

@ -0,0 +1,8 @@
# Framed app for actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-framed)](https://crates.io/crates/actix-framed) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & community resources
* [API Documentation](https://docs.rs/actix-framed/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-framed](https://crates.io/crates/actix-framed)
* Minimum supported Rust version: 1.33 or later

24
actix-framed/changes.md Normal file
View File

@ -0,0 +1,24 @@
# Changes
## [0.3.0] - 2019-12-25
* Migrate to actix-http 1.0
## [0.2.1] - 2019-07-20
* Remove unneeded actix-utils dependency
## [0.2.0] - 2019-05-12
* Update dependencies
## [0.1.0] - 2019-04-16
* Update tests
## [0.1.0-alpha.1] - 2019-04-12
* Initial release

221
actix-framed/src/app.rs Normal file
View File

@ -0,0 +1,221 @@
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_http::h1::{Codec, SendResponse};
use actix_http::{Error, Request, Response};
use actix_router::{Path, Router, Url};
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
use futures::future::{ok, FutureExt, LocalBoxFuture};
use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService};
use crate::request::FramedRequest;
use crate::state::State;
type BoxedResponse = LocalBoxFuture<'static, Result<(), Error>>;
pub trait HttpServiceFactory {
type Factory: ServiceFactory;
fn path(&self) -> &str;
fn create(self) -> Self::Factory;
}
/// Application builder
pub struct FramedApp<T, S = ()> {
state: State<S>,
services: Vec<(String, BoxedHttpNewService<FramedRequest<T, S>>)>,
}
impl<T: 'static> FramedApp<T, ()> {
pub fn new() -> Self {
FramedApp {
state: State::new(()),
services: Vec::new(),
}
}
}
impl<T: 'static, S: 'static> FramedApp<T, S> {
pub fn with(state: S) -> FramedApp<T, S> {
FramedApp {
services: Vec::new(),
state: State::new(state),
}
}
pub fn service<U>(mut self, factory: U) -> Self
where
U: HttpServiceFactory,
U::Factory: ServiceFactory<
Config = (),
Request = FramedRequest<T, S>,
Response = (),
Error = Error,
InitError = (),
> + 'static,
<U::Factory as ServiceFactory>::Future: 'static,
<U::Factory as ServiceFactory>::Service: Service<
Request = FramedRequest<T, S>,
Response = (),
Error = Error,
Future = LocalBoxFuture<'static, Result<(), Error>>,
>,
{
let path = factory.path().to_string();
self.services
.push((path, Box::new(HttpNewService::new(factory.create()))));
self
}
}
impl<T, S> IntoServiceFactory<FramedAppFactory<T, S>> for FramedApp<T, S>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
S: 'static,
{
fn into_factory(self) -> FramedAppFactory<T, S> {
FramedAppFactory {
state: self.state,
services: Rc::new(self.services),
}
}
}
#[derive(Clone)]
pub struct FramedAppFactory<T, S> {
state: State<S>,
services: Rc<Vec<(String, BoxedHttpNewService<FramedRequest<T, S>>)>>,
}
impl<T, S> ServiceFactory for FramedAppFactory<T, S>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
S: 'static,
{
type Config = ();
type Request = (Request, Framed<T, Codec>);
type Response = ();
type Error = Error;
type InitError = ();
type Service = FramedAppService<T, S>;
type Future = CreateService<T, S>;
fn new_service(&self, _: ()) -> Self::Future {
CreateService {
fut: self
.services
.iter()
.map(|(path, service)| {
CreateServiceItem::Future(
Some(path.clone()),
service.new_service(()),
)
})
.collect(),
state: self.state.clone(),
}
}
}
#[doc(hidden)]
pub struct CreateService<T, S> {
fut: Vec<CreateServiceItem<T, S>>,
state: State<S>,
}
enum CreateServiceItem<T, S> {
Future(
Option<String>,
LocalBoxFuture<'static, Result<BoxedHttpService<FramedRequest<T, S>>, ()>>,
),
Service(String, BoxedHttpService<FramedRequest<T, S>>),
}
impl<S: 'static, T: 'static> Future for CreateService<T, S>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Output = Result<FramedAppService<T, S>, ()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let mut done = true;
// poll http services
for item in &mut self.fut {
let res = match item {
CreateServiceItem::Future(ref mut path, ref mut fut) => {
match Pin::new(fut).poll(cx) {
Poll::Ready(Ok(service)) => {
Some((path.take().unwrap(), service))
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => {
done = false;
None
}
}
}
CreateServiceItem::Service(_, _) => continue,
};
if let Some((path, service)) = res {
*item = CreateServiceItem::Service(path, service);
}
}
if done {
let router = self
.fut
.drain(..)
.fold(Router::build(), |mut router, item| {
match item {
CreateServiceItem::Service(path, service) => {
router.path(&path, service);
}
CreateServiceItem::Future(_, _) => unreachable!(),
}
router
});
Poll::Ready(Ok(FramedAppService {
router: router.finish(),
state: self.state.clone(),
}))
} else {
Poll::Pending
}
}
}
pub struct FramedAppService<T, S> {
state: State<S>,
router: Router<BoxedHttpService<FramedRequest<T, S>>>,
}
impl<S: 'static, T: 'static> Service for FramedAppService<T, S>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Request = (Request, Framed<T, Codec>);
type Response = ();
type Error = Error;
type Future = BoxedResponse;
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, (req, framed): (Request, Framed<T, Codec>)) -> Self::Future {
let mut path = Path::new(Url::new(req.uri().clone()));
if let Some((srv, _info)) = self.router.recognize_mut(&mut path) {
return srv.call(FramedRequest::new(req, framed, path, self.state.clone()));
}
SendResponse::new(framed, Response::NotFound().finish())
.then(|_| ok(()))
.boxed_local()
}
}

View File

@ -0,0 +1,98 @@
use std::task::{Context, Poll};
use actix_http::Error;
use actix_service::{Service, ServiceFactory};
use futures::future::{FutureExt, LocalBoxFuture};
pub(crate) type BoxedHttpService<Req> = Box<
dyn Service<
Request = Req,
Response = (),
Error = Error,
Future = LocalBoxFuture<'static, Result<(), Error>>,
>,
>;
pub(crate) type BoxedHttpNewService<Req> = Box<
dyn ServiceFactory<
Config = (),
Request = Req,
Response = (),
Error = Error,
InitError = (),
Service = BoxedHttpService<Req>,
Future = LocalBoxFuture<'static, Result<BoxedHttpService<Req>, ()>>,
>,
>;
pub(crate) struct HttpNewService<T: ServiceFactory>(T);
impl<T> HttpNewService<T>
where
T: ServiceFactory<Response = (), Error = Error>,
T::Response: 'static,
T::Future: 'static,
T::Service: Service<Future = LocalBoxFuture<'static, Result<(), Error>>> + 'static,
<T::Service as Service>::Future: 'static,
{
pub fn new(service: T) -> Self {
HttpNewService(service)
}
}
impl<T> ServiceFactory for HttpNewService<T>
where
T: ServiceFactory<Config = (), Response = (), Error = Error>,
T::Request: 'static,
T::Future: 'static,
T::Service: Service<Future = LocalBoxFuture<'static, Result<(), Error>>> + 'static,
<T::Service as Service>::Future: 'static,
{
type Config = ();
type Request = T::Request;
type Response = ();
type Error = Error;
type InitError = ();
type Service = BoxedHttpService<T::Request>;
type Future = LocalBoxFuture<'static, Result<Self::Service, ()>>;
fn new_service(&self, _: ()) -> Self::Future {
let fut = self.0.new_service(());
async move {
fut.await.map_err(|_| ()).map(|service| {
let service: BoxedHttpService<_> =
Box::new(HttpServiceWrapper { service });
service
})
}
.boxed_local()
}
}
struct HttpServiceWrapper<T: Service> {
service: T,
}
impl<T> Service for HttpServiceWrapper<T>
where
T: Service<
Response = (),
Future = LocalBoxFuture<'static, Result<(), Error>>,
Error = Error,
>,
T::Request: 'static,
{
type Request = T::Request;
type Response = ();
type Error = Error;
type Future = LocalBoxFuture<'static, Result<(), Error>>;
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: Self::Request) -> Self::Future {
self.service.call(req)
}
}

17
actix-framed/src/lib.rs Normal file
View File

@ -0,0 +1,17 @@
#![allow(clippy::type_complexity, clippy::new_without_default, dead_code)]
mod app;
mod helpers;
mod request;
mod route;
mod service;
mod state;
pub mod test;
// re-export for convinience
pub use actix_http::{http, Error, HttpMessage, Response, ResponseError};
pub use self::app::{FramedApp, FramedAppService};
pub use self::request::FramedRequest;
pub use self::route::FramedRoute;
pub use self::service::{SendError, VerifyWebSockets};
pub use self::state::State;

172
actix-framed/src/request.rs Normal file
View File

@ -0,0 +1,172 @@
use std::cell::{Ref, RefMut};
use actix_codec::Framed;
use actix_http::http::{HeaderMap, Method, Uri, Version};
use actix_http::{h1::Codec, Extensions, Request, RequestHead};
use actix_router::{Path, Url};
use crate::state::State;
pub struct FramedRequest<Io, S = ()> {
req: Request,
framed: Framed<Io, Codec>,
state: State<S>,
pub(crate) path: Path<Url>,
}
impl<Io, S> FramedRequest<Io, S> {
pub fn new(
req: Request,
framed: Framed<Io, Codec>,
path: Path<Url>,
state: State<S>,
) -> Self {
Self {
req,
framed,
state,
path,
}
}
}
impl<Io, S> FramedRequest<Io, S> {
/// Split request into a parts
pub fn into_parts(self) -> (Request, Framed<Io, Codec>, State<S>) {
(self.req, self.framed, self.state)
}
/// This method returns reference to the request head
#[inline]
pub fn head(&self) -> &RequestHead {
self.req.head()
}
/// This method returns muttable reference to the request head.
/// panics if multiple references of http request exists.
#[inline]
pub fn head_mut(&mut self) -> &mut RequestHead {
self.req.head_mut()
}
/// Shared application state
#[inline]
pub fn state(&self) -> &S {
self.state.get_ref()
}
/// Request's uri.
#[inline]
pub fn uri(&self) -> &Uri {
&self.head().uri
}
/// Read the Request method.
#[inline]
pub fn method(&self) -> &Method {
&self.head().method
}
/// Read the Request Version.
#[inline]
pub fn version(&self) -> Version {
self.head().version
}
#[inline]
/// Returns request's headers.
pub fn headers(&self) -> &HeaderMap {
&self.head().headers
}
/// The target path of this Request.
#[inline]
pub fn path(&self) -> &str {
self.head().uri.path()
}
/// The query string in the URL.
///
/// E.g., id=10
#[inline]
pub fn query_string(&self) -> &str {
if let Some(query) = self.uri().query().as_ref() {
query
} else {
""
}
}
/// Get a reference to the Path parameters.
///
/// Params is a container for url parameters.
/// A variable segment is specified in the form `{identifier}`,
/// where the identifier can be used later in a request handler to
/// access the matched value for that segment.
#[inline]
pub fn match_info(&self) -> &Path<Url> {
&self.path
}
/// Request extensions
#[inline]
pub fn extensions(&self) -> Ref<Extensions> {
self.head().extensions()
}
/// Mutable reference to a the request's extensions
#[inline]
pub fn extensions_mut(&self) -> RefMut<Extensions> {
self.head().extensions_mut()
}
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use actix_http::http::{HeaderName, HeaderValue};
use actix_http::test::{TestBuffer, TestRequest};
use super::*;
#[test]
fn test_reqest() {
let buf = TestBuffer::empty();
let framed = Framed::new(buf, Codec::default());
let req = TestRequest::with_uri("/index.html?q=1")
.header("content-type", "test")
.finish();
let path = Path::new(Url::new(req.uri().clone()));
let mut freq = FramedRequest::new(req, framed, path, State::new(10u8));
assert_eq!(*freq.state(), 10);
assert_eq!(freq.version(), Version::HTTP_11);
assert_eq!(freq.method(), Method::GET);
assert_eq!(freq.path(), "/index.html");
assert_eq!(freq.query_string(), "q=1");
assert_eq!(
freq.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"test"
);
freq.head_mut().headers.insert(
HeaderName::try_from("x-hdr").unwrap(),
HeaderValue::from_static("test"),
);
assert_eq!(
freq.headers().get("x-hdr").unwrap().to_str().unwrap(),
"test"
);
freq.extensions_mut().insert(100usize);
assert_eq!(*freq.extensions().get::<usize>().unwrap(), 100usize);
let (_, _, state) = freq.into_parts();
assert_eq!(*state, 10);
}
}

159
actix-framed/src/route.rs Normal file
View File

@ -0,0 +1,159 @@
use std::fmt;
use std::future::Future;
use std::marker::PhantomData;
use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite};
use actix_http::{http::Method, Error};
use actix_service::{Service, ServiceFactory};
use futures::future::{ok, FutureExt, LocalBoxFuture, Ready};
use log::error;
use crate::app::HttpServiceFactory;
use crate::request::FramedRequest;
/// Resource route definition
///
/// Route uses builder-like pattern for configuration.
/// If handler is not explicitly set, default *404 Not Found* handler is used.
pub struct FramedRoute<Io, S, F = (), R = (), E = ()> {
handler: F,
pattern: String,
methods: Vec<Method>,
state: PhantomData<(Io, S, R, E)>,
}
impl<Io, S> FramedRoute<Io, S> {
pub fn new(pattern: &str) -> Self {
FramedRoute {
handler: (),
pattern: pattern.to_string(),
methods: Vec::new(),
state: PhantomData,
}
}
pub fn get(path: &str) -> FramedRoute<Io, S> {
FramedRoute::new(path).method(Method::GET)
}
pub fn post(path: &str) -> FramedRoute<Io, S> {
FramedRoute::new(path).method(Method::POST)
}
pub fn put(path: &str) -> FramedRoute<Io, S> {
FramedRoute::new(path).method(Method::PUT)
}
pub fn delete(path: &str) -> FramedRoute<Io, S> {
FramedRoute::new(path).method(Method::DELETE)
}
pub fn method(mut self, method: Method) -> Self {
self.methods.push(method);
self
}
pub fn to<F, R, E>(self, handler: F) -> FramedRoute<Io, S, F, R, E>
where
F: FnMut(FramedRequest<Io, S>) -> R,
R: Future<Output = Result<(), E>> + 'static,
E: fmt::Debug,
{
FramedRoute {
handler,
pattern: self.pattern,
methods: self.methods,
state: PhantomData,
}
}
}
impl<Io, S, F, R, E> HttpServiceFactory for FramedRoute<Io, S, F, R, E>
where
Io: AsyncRead + AsyncWrite + 'static,
F: FnMut(FramedRequest<Io, S>) -> R + Clone,
R: Future<Output = Result<(), E>> + 'static,
E: fmt::Display,
{
type Factory = FramedRouteFactory<Io, S, F, R, E>;
fn path(&self) -> &str {
&self.pattern
}
fn create(self) -> Self::Factory {
FramedRouteFactory {
handler: self.handler,
methods: self.methods,
_t: PhantomData,
}
}
}
pub struct FramedRouteFactory<Io, S, F, R, E> {
handler: F,
methods: Vec<Method>,
_t: PhantomData<(Io, S, R, E)>,
}
impl<Io, S, F, R, E> ServiceFactory for FramedRouteFactory<Io, S, F, R, E>
where
Io: AsyncRead + AsyncWrite + 'static,
F: FnMut(FramedRequest<Io, S>) -> R + Clone,
R: Future<Output = Result<(), E>> + 'static,
E: fmt::Display,
{
type Config = ();
type Request = FramedRequest<Io, S>;
type Response = ();
type Error = Error;
type InitError = ();
type Service = FramedRouteService<Io, S, F, R, E>;
type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
ok(FramedRouteService {
handler: self.handler.clone(),
methods: self.methods.clone(),
_t: PhantomData,
})
}
}
pub struct FramedRouteService<Io, S, F, R, E> {
handler: F,
methods: Vec<Method>,
_t: PhantomData<(Io, S, R, E)>,
}
impl<Io, S, F, R, E> Service for FramedRouteService<Io, S, F, R, E>
where
Io: AsyncRead + AsyncWrite + 'static,
F: FnMut(FramedRequest<Io, S>) -> R + Clone,
R: Future<Output = Result<(), E>> + 'static,
E: fmt::Display,
{
type Request = FramedRequest<Io, S>;
type Response = ();
type Error = Error;
type Future = LocalBoxFuture<'static, Result<(), Error>>;
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: FramedRequest<Io, S>) -> Self::Future {
let fut = (self.handler)(req);
async move {
let res = fut.await;
if let Err(e) = res {
error!("Error in request handler: {}", e);
}
Ok(())
}
.boxed_local()
}
}

156
actix-framed/src/service.rs Normal file
View File

@ -0,0 +1,156 @@
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_http::body::BodySize;
use actix_http::error::ResponseError;
use actix_http::h1::{Codec, Message};
use actix_http::ws::{verify_handshake, HandshakeError};
use actix_http::{Request, Response};
use actix_service::{Service, ServiceFactory};
use futures::future::{err, ok, Either, Ready};
use futures::Future;
/// Service that verifies incoming request if it is valid websocket
/// upgrade request. In case of error returns `HandshakeError`
pub struct VerifyWebSockets<T, C> {
_t: PhantomData<(T, C)>,
}
impl<T, C> Default for VerifyWebSockets<T, C> {
fn default() -> Self {
VerifyWebSockets { _t: PhantomData }
}
}
impl<T, C> ServiceFactory for VerifyWebSockets<T, C> {
type Config = C;
type Request = (Request, Framed<T, Codec>);
type Response = (Request, Framed<T, Codec>);
type Error = (HandshakeError, Framed<T, Codec>);
type InitError = ();
type Service = VerifyWebSockets<T, C>;
type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: C) -> Self::Future {
ok(VerifyWebSockets { _t: PhantomData })
}
}
impl<T, C> Service for VerifyWebSockets<T, C> {
type Request = (Request, Framed<T, Codec>);
type Response = (Request, Framed<T, Codec>);
type Error = (HandshakeError, Framed<T, Codec>);
type Future = Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, (req, framed): (Request, Framed<T, Codec>)) -> Self::Future {
match verify_handshake(req.head()) {
Err(e) => err((e, framed)),
Ok(_) => ok((req, framed)),
}
}
}
/// Send http/1 error response
pub struct SendError<T, R, E, C>(PhantomData<(T, R, E, C)>);
impl<T, R, E, C> Default for SendError<T, R, E, C>
where
T: AsyncRead + AsyncWrite,
E: ResponseError,
{
fn default() -> Self {
SendError(PhantomData)
}
}
impl<T, R, E, C> ServiceFactory for SendError<T, R, E, C>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
R: 'static,
E: ResponseError + 'static,
{
type Config = C;
type Request = Result<R, (E, Framed<T, Codec>)>;
type Response = R;
type Error = (E, Framed<T, Codec>);
type InitError = ();
type Service = SendError<T, R, E, C>;
type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: C) -> Self::Future {
ok(SendError(PhantomData))
}
}
impl<T, R, E, C> Service for SendError<T, R, E, C>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
R: 'static,
E: ResponseError + 'static,
{
type Request = Result<R, (E, Framed<T, Codec>)>;
type Response = R;
type Error = (E, Framed<T, Codec>);
type Future = Either<Ready<Result<R, (E, Framed<T, Codec>)>>, SendErrorFut<T, R, E>>;
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Result<R, (E, Framed<T, Codec>)>) -> Self::Future {
match req {
Ok(r) => Either::Left(ok(r)),
Err((e, framed)) => {
let res = e.error_response().drop_body();
Either::Right(SendErrorFut {
framed: Some(framed),
res: Some((res, BodySize::Empty).into()),
err: Some(e),
_t: PhantomData,
})
}
}
}
}
#[pin_project::pin_project]
pub struct SendErrorFut<T, R, E> {
res: Option<Message<(Response<()>, BodySize)>>,
framed: Option<Framed<T, Codec>>,
err: Option<E>,
_t: PhantomData<R>,
}
impl<T, R, E> Future for SendErrorFut<T, R, E>
where
E: ResponseError,
T: AsyncRead + AsyncWrite + Unpin,
{
type Output = Result<R, (E, Framed<T, Codec>)>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
if let Some(res) = self.res.take() {
if self.framed.as_mut().unwrap().write(res).is_err() {
return Poll::Ready(Err((
self.err.take().unwrap(),
self.framed.take().unwrap(),
)));
}
}
match self.framed.as_mut().unwrap().flush(cx) {
Poll::Ready(Ok(_)) => {
Poll::Ready(Err((self.err.take().unwrap(), self.framed.take().unwrap())))
}
Poll::Ready(Err(_)) => {
Poll::Ready(Err((self.err.take().unwrap(), self.framed.take().unwrap())))
}
Poll::Pending => Poll::Pending,
}
}
}

29
actix-framed/src/state.rs Normal file
View File

@ -0,0 +1,29 @@
use std::ops::Deref;
use std::sync::Arc;
/// Application state
pub struct State<S>(Arc<S>);
impl<S> State<S> {
pub fn new(state: S) -> State<S> {
State(Arc::new(state))
}
pub fn get_ref(&self) -> &S {
self.0.as_ref()
}
}
impl<S> Deref for State<S> {
type Target = S;
fn deref(&self) -> &S {
self.0.as_ref()
}
}
impl<S> Clone for State<S> {
fn clone(&self) -> State<S> {
State(self.0.clone())
}
}

155
actix-framed/src/test.rs Normal file
View File

@ -0,0 +1,155 @@
//! Various helpers for Actix applications to use during testing.
use std::convert::TryFrom;
use std::future::Future;
use actix_codec::Framed;
use actix_http::h1::Codec;
use actix_http::http::header::{Header, HeaderName, IntoHeaderValue};
use actix_http::http::{Error as HttpError, Method, Uri, Version};
use actix_http::test::{TestBuffer, TestRequest as HttpTestRequest};
use actix_router::{Path, Url};
use crate::{FramedRequest, State};
/// Test `Request` builder.
pub struct TestRequest<S = ()> {
req: HttpTestRequest,
path: Path<Url>,
state: State<S>,
}
impl Default for TestRequest<()> {
fn default() -> TestRequest {
TestRequest {
req: HttpTestRequest::default(),
path: Path::new(Url::new(Uri::default())),
state: State::new(()),
}
}
}
impl TestRequest<()> {
/// Create TestRequest and set request uri
pub fn with_uri(path: &str) -> Self {
Self::get().uri(path)
}
/// Create TestRequest and set header
pub fn with_hdr<H: Header>(hdr: H) -> Self {
Self::default().set(hdr)
}
/// Create TestRequest and set header
pub fn with_header<K, V>(key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue,
{
Self::default().header(key, value)
}
/// Create TestRequest and set method to `Method::GET`
pub fn get() -> Self {
Self::default().method(Method::GET)
}
/// Create TestRequest and set method to `Method::POST`
pub fn post() -> Self {
Self::default().method(Method::POST)
}
}
impl<S> TestRequest<S> {
/// Create TestRequest and set request uri
pub fn with_state(state: S) -> TestRequest<S> {
let req = TestRequest::get();
TestRequest {
state: State::new(state),
req: req.req,
path: req.path,
}
}
/// Set HTTP version of this request
pub fn version(mut self, ver: Version) -> Self {
self.req.version(ver);
self
}
/// Set HTTP method of this request
pub fn method(mut self, meth: Method) -> Self {
self.req.method(meth);
self
}
/// Set HTTP Uri of this request
pub fn uri(mut self, path: &str) -> Self {
self.req.uri(path);
self
}
/// Set a header
pub fn set<H: Header>(mut self, hdr: H) -> Self {
self.req.set(hdr);
self
}
/// Set a header
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue,
{
self.req.header(key, value);
self
}
/// Set request path pattern parameter
pub fn param(mut self, name: &'static str, value: &'static str) -> Self {
self.path.add_static(name, value);
self
}
/// Complete request creation and generate `Request` instance
pub fn finish(mut self) -> FramedRequest<TestBuffer, S> {
let req = self.req.finish();
self.path.get_mut().update(req.uri());
let framed = Framed::new(TestBuffer::empty(), Codec::default());
FramedRequest::new(req, framed, self.path, self.state)
}
/// This method generates `FramedRequest` instance and executes async handler
pub async fn run<F, R, I, E>(self, f: F) -> Result<I, E>
where
F: FnOnce(FramedRequest<TestBuffer, S>) -> R,
R: Future<Output = Result<I, E>>,
{
f(self.finish()).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test() {
let req = TestRequest::with_uri("/index.html")
.header("x-test", "test")
.param("test", "123")
.finish();
assert_eq!(*req.state(), ());
assert_eq!(req.version(), Version::HTTP_11);
assert_eq!(req.method(), Method::GET);
assert_eq!(req.path(), "/index.html");
assert_eq!(req.query_string(), "");
assert_eq!(
req.headers().get("x-test").unwrap().to_str().unwrap(),
"test"
);
assert_eq!(&req.match_info()["test"], "123");
}
}

View File

@ -0,0 +1,159 @@
use actix_codec::{AsyncRead, AsyncWrite};
use actix_http::{body, http::StatusCode, ws, Error, HttpService, Response};
use actix_http_test::test_server;
use actix_service::{pipeline_factory, IntoServiceFactory, ServiceFactory};
use actix_utils::framed::Dispatcher;
use bytes::Bytes;
use futures::{future, SinkExt, StreamExt};
use actix_framed::{FramedApp, FramedRequest, FramedRoute, SendError, VerifyWebSockets};
async fn ws_service<T: AsyncRead + AsyncWrite>(
req: FramedRequest<T>,
) -> Result<(), Error> {
let (req, mut framed, _) = req.into_parts();
let res = ws::handshake(req.head()).unwrap().message_body(());
framed
.send((res, body::BodySize::None).into())
.await
.unwrap();
Dispatcher::new(framed.into_framed(ws::Codec::new()), service)
.await
.unwrap();
Ok(())
}
async fn service(msg: ws::Frame) -> Result<ws::Message, Error> {
let msg = match msg {
ws::Frame::Ping(msg) => ws::Message::Pong(msg),
ws::Frame::Text(text) => {
ws::Message::Text(String::from_utf8_lossy(&text).to_string())
}
ws::Frame::Binary(bin) => ws::Message::Binary(bin),
ws::Frame::Close(reason) => ws::Message::Close(reason),
_ => panic!(),
};
Ok(msg)
}
#[actix_rt::test]
async fn test_simple() {
let mut srv = test_server(|| {
HttpService::build()
.upgrade(
FramedApp::new().service(FramedRoute::get("/index.html").to(ws_service)),
)
.finish(|_| future::ok::<_, Error>(Response::NotFound()))
.tcp()
});
assert!(srv.ws_at("/test").await.is_err());
// client service
let mut framed = srv.ws_at("/index.html").await.unwrap();
framed
.send(ws::Message::Text("text".to_string()))
.await
.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Text(Bytes::from_static(b"text"))
);
framed
.send(ws::Message::Binary("text".into()))
.await
.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Binary(Bytes::from_static(b"text"))
);
framed.send(ws::Message::Ping("text".into())).await.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Pong("text".to_string().into())
);
framed
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
.await
.unwrap();
let (item, _) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Close(Some(ws::CloseCode::Normal.into()))
);
}
#[actix_rt::test]
async fn test_service() {
let mut srv = test_server(|| {
pipeline_factory(actix_http::h1::OneRequest::new().map_err(|_| ())).and_then(
pipeline_factory(
pipeline_factory(VerifyWebSockets::default())
.then(SendError::default())
.map_err(|_| ()),
)
.and_then(
FramedApp::new()
.service(FramedRoute::get("/index.html").to(ws_service))
.into_factory()
.map_err(|_| ()),
),
)
});
// non ws request
let res = srv.get("/index.html").send().await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// not found
assert!(srv.ws_at("/test").await.is_err());
// client service
let mut framed = srv.ws_at("/index.html").await.unwrap();
framed
.send(ws::Message::Text("text".to_string()))
.await
.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Text(Bytes::from_static(b"text"))
);
framed
.send(ws::Message::Binary("text".into()))
.await
.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Binary(Bytes::from_static(b"text"))
);
framed.send(ws::Message::Ping("text".into())).await.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Pong("text".to_string().into())
);
framed
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
.await
.unwrap();
let (item, _) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Close(Some(ws::CloseCode::Normal.into()))
);
}

View File

@ -1,5 +1,308 @@
# Changes # Changes
## [1.0.1] - 2019-12-20
### Fixed
* Poll upgrade service's readiness from HTTP service handlers
* Replace brotli with brotli2 #1224
## [1.0.0] - 2019-12-13
### Added
* Add websockets continuation frame support
### Changed
* Replace `flate2-xxx` features with `compress`
## [1.0.0-alpha.5] - 2019-12-09
### Fixed
* Check `Upgrade` service readiness before calling it
* Fix buffer remaining capacity calcualtion
### Changed
* Websockets: Ping and Pong should have binary data #1049
## [1.0.0-alpha.4] - 2019-12-08
### Added
* Add impl ResponseBuilder for Error
### Changed
* Use rust based brotli compression library
## [1.0.0-alpha.3] - 2019-12-07
### Changed
* Migrate to tokio 0.2
* Migrate to `std::future`
## [0.2.11] - 2019-11-06
### Added
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
### Fixed
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header #1118
## [0.2.10] - 2019-09-11
### Added
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead`
### Fixed
* h2 will use error response #1080
* on_connect result isn't added to request extensions for http2 requests #1009
## [0.2.9] - 2019-08-13
### Changed
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
* Update percent-encoding to 2.1
* Update serde_urlencoded to 0.6.1
### Fixed
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
## [0.2.8] - 2019-08-01
### Added
* Add `rustls` support
* Add `Clone` impl for `HeaderMap`
### Fixed
* awc client panic #1016
* Invalid response with compression middleware enabled, but compression-related features disabled #997
## [0.2.7] - 2019-07-18
### Added
* Add support for downcasting response errors #986
## [0.2.6] - 2019-07-17
### Changed
* Replace `ClonableService` with local copy
* Upgrade `rand` dependency version to 0.7
## [0.2.5] - 2019-06-28
### Added
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
### Changed
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
* Add `Copy` and `Clone` impls for `ws::Codec`
## [0.2.4] - 2019-06-16
### Fixed
* Do not compress NoContent (204) responses #918
## [0.2.3] - 2019-06-02
### Added
* Debug impl for ResponseBuilder
* From SizedStream and BodyStream for Body
### Changed
* SizedStream uses u64
## [0.2.2] - 2019-05-29
### Fixed
* Parse incoming stream before closing stream on disconnect #868
## [0.2.1] - 2019-05-25
### Fixed
* Handle socket read disconnect
## [0.2.0] - 2019-05-12
### Changed
* Update actix-service to 0.4
* Expect and upgrade services accept `ServerConfig` config.
### Deleted
* `OneRequest` service
## [0.1.5] - 2019-05-04
### Fixed
* Clean up response extensions in response pool #817
## [0.1.4] - 2019-04-24
### Added
* Allow to render h1 request headers in `Camel-Case`
### Fixed
* Read until eof for http/1.0 responses #771
## [0.1.3] - 2019-04-23
### Fixed
* Fix http client pool management
* Fix http client wait queue management #794
## [0.1.2] - 2019-04-23
### Fixed
* Fix BorrowMutError panic in client connector #793
## [0.1.1] - 2019-04-19
### Changed
* Cookie::max_age() accepts value in seconds
* Cookie::max_age_time() accepts value in time::Duration
* Allow to specify server address for client connector
## [0.1.0] - 2019-04-16
### Added
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
### Changed
* `actix_http::encoding` always available
* use trust-dns-resolver 0.11.0
## [0.1.0-alpha.5] - 2019-04-12
### Added
* Allow to use custom service for upgrade requests
* Added `h1::SendResponse` future.
### Changed
* MessageBody::length() renamed to MessageBody::size() for consistency
* ws handshake verification functions take RequestHead instead of Request
## [0.1.0-alpha.4] - 2019-04-08
### Added
* Allow to use custom `Expect` handler
* Add minimal `std::error::Error` impl for `Error`
### Changed
* Export IntoHeaderValue
* Render error and return as response body
* Use thread pool for response body comression
### Deleted
* Removed PayloadBuffer
## [0.1.0-alpha.3] - 2019-04-02
### Added
* Warn when an unsealed private cookie isn't valid UTF-8
### Fixed
* Rust 1.31.0 compatibility
* Preallocate read buffer for h1 codec
* Detect socket disconnection during protocol selection
## [0.1.0-alpha.2] - 2019-03-29
### Added
* Added ws::Message::Nop, no-op websockets message
### Changed
* 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,26 +1,21 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "0.1.0-alpha.1" version = "1.0.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix http primitives" description = "Actix http primitives"
readme = "README.md" readme = "README.md"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-http.git" repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-http/" documentation = "https://docs.rs/actix-http/"
categories = ["network-programming", "asynchronous", categories = ["network-programming", "asynchronous",
"web-programming::http-server", "web-programming::http-server",
"web-programming::websocket"] "web-programming::websocket"]
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
edition = "2018" edition = "2018"
workspace = ".."
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["ssl", "fail", "cookie", "brotli", "flate2-zlib"] features = ["openssl", "rustls", "failure", "compress", "secure-cookies"]
[badges]
travis-ci = { repository = "actix/actix-web", branch = "master" }
codecov = { repository = "actix/actix-web", branch = "master", service = "github" }
[lib] [lib]
name = "actix_http" name = "actix_http"
@ -30,77 +25,77 @@ path = "src/lib.rs"
default = [] default = []
# openssl # openssl
ssl = ["openssl", "actix-connect/ssl"] openssl = ["actix-tls/openssl", "actix-connect/openssl"]
# cookies integration # rustls support
cookies = ["cookie"] rustls = ["actix-tls/rustls", "actix-connect/rustls"]
# brotli encoding, requires c compiler # enable compressison support
brotli = ["brotli2"] compress = ["flate2", "brotli2"]
# miniz-sys backend for flate2 crate
flate2-zlib = ["flate2/miniz-sys"]
# rust backend for flate2 crate
flate2-rust = ["flate2/rust_backend"]
# failure integration. actix does not use failure anymore # failure integration. actix does not use failure anymore
fail = ["failure"] failure = ["fail-ure"]
# support for secure cookies
secure-cookies = ["ring"]
[dependencies] [dependencies]
actix-service = "0.3.4" actix-service = "1.0.1"
actix-codec = "0.1.2" actix-codec = "0.2.0"
actix-connect = "0.1.0" actix-connect = "1.0.1"
actix-utils = "0.3.4" actix-utils = "1.0.3"
actix-server-config = "0.1.0" actix-rt = "1.0.0"
actix-threadpool = "0.1.0" actix-threadpool = "0.3.1"
actix-tls = { version = "1.0.0", optional = true }
base64 = "0.10" base64 = "0.11"
bitflags = "1.0" bitflags = "1.2"
bytes = "0.4" bytes = "0.5.3"
byteorder = "1.2" copyless = "0.1.4"
derive_more = "0.14" chrono = "0.4.6"
encoding = "0.2" derive_more = "0.99.2"
futures = "0.1" either = "1.5.3"
hashbrown = "0.1.8" encoding_rs = "0.8"
h2 = "0.1.16" futures-core = "0.3.1"
http = "0.1.16" futures-util = "0.3.1"
futures-channel = "0.3.1"
fxhash = "0.2.1"
h2 = "0.2.1"
http = "0.2.0"
httparse = "1.3" httparse = "1.3"
indexmap = "1.0" indexmap = "1.3"
lazy_static = "1.0" lazy_static = "1.4"
language-tags = "0.2" language-tags = "0.2"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "1.0" percent-encoding = "2.1"
rand = "0.6" pin-project = "0.4.6"
regex = "1.0" rand = "0.7"
regex = "1.3"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
sha1 = "0.6" sha1 = "0.6"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.5.3" serde_urlencoded = "0.6.1"
time = "0.1" time = "0.1.42"
tokio-tcp = "0.1.3"
tokio-timer = "0.2" # for secure cookie
tokio-current-thread = "0.1" ring = { version = "0.16.9", optional = true }
trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false }
# compression # compression
brotli2 = { version="^0.3.2", optional = true } brotli2 = { version="0.3.2", optional = true }
flate2 = { version="^1.0.2", optional = true, default-features = false } flate2 = { version = "1.0.13", optional = true }
# optional deps # optional deps
cookie = { version="0.11", features=["percent-encode"], optional = true } fail-ure = { version = "0.1.5", package="failure", optional = true }
failure = { version = "0.1.5", optional = true }
openssl = { version="0.10", optional = true }
[dev-dependencies] [dev-dependencies]
actix-rt = "0.2.2" actix-server = "1.0.0"
actix-server = { version = "0.4.0", features=["ssl"] } actix-connect = { version = "1.0.0", features=["openssl"] }
actix-connect = { version = "0.1.0", features=["ssl"] } actix-http-test = { version = "1.0.0", features=["openssl"] }
actix-http-test = { path="../test-server", features=["ssl"] } actix-tls = { version = "1.0.0", features=["openssl"] }
futures = "0.3.1"
env_logger = "0.6" env_logger = "0.6"
serde_derive = "1.0" serde_derive = "1.0"
openssl = { version="0.10" } open-ssl = { version="0.10", package = "openssl" }
tokio-tcp = "0.1" rust-tls = { version="0.16", package = "rustls" }

View File

@ -1,4 +1,4 @@
# Actix http [![Build Status](https://travis-ci.org/actix/actix-http.svg?branch=master)](https://travis-ci.org/actix/actix-http) [![codecov](https://codecov.io/gh/actix/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # Actix http [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-http)](https://crates.io/crates/actix-http) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Actix http Actix http
@ -8,7 +8,7 @@ Actix http
* [API Documentation](https://docs.rs/actix-http/) * [API Documentation](https://docs.rs/actix-http/)
* [Chat on gitter](https://gitter.im/actix/actix) * [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-http](https://crates.io/crates/actix-http) * Cargo package: [actix-http](https://crates.io/crates/actix-http)
* Minimum supported Rust version: 1.26 or later * Minimum supported Rust version: 1.31 or later
## Example ## Example

View File

@ -1,9 +1,9 @@
use std::{env, io}; use std::{env, io};
use actix_http::{error::PayloadError, 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::{Future, Stream}; use futures::StreamExt;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info; use log::info;
@ -17,21 +17,24 @@ fn main() -> io::Result<()> {
.client_timeout(1000) .client_timeout(1000)
.client_disconnect(1000) .client_disconnect(1000)
.finish(|mut req: Request| { .finish(|mut req: Request| {
req.take_payload() async move {
.fold(BytesMut::new(), move |mut body, chunk| { let mut body = BytesMut::new();
body.extend_from_slice(&chunk); while let Some(item) = req.payload().next().await {
Ok::<_, PayloadError>(body) body.extend_from_slice(&item?);
}) }
.and_then(|bytes| {
info!("request body: {:?}", bytes); info!("request body: {:?}", body);
let mut res = Response::Ok(); Ok::<_, Error>(
res.header( Response::Ok()
"x-head", .header(
HeaderValue::from_static("dummy value!"), "x-head",
); HeaderValue::from_static("dummy value!"),
Ok(res.body(bytes)) )
}) .body(body),
)
}
}) })
.tcp()
})? })?
.run() .run()
} }

View File

@ -1,25 +1,22 @@
use std::{env, io}; use std::{env, io};
use actix_http::http::HeaderValue; use actix_http::http::HeaderValue;
use actix_http::{error::PayloadError, 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::{Future, Stream}; use futures::StreamExt;
use log::info; use log::info;
fn handle_request(mut req: Request) -> impl Future<Item = Response, Error = Error> { async fn handle_request(mut req: Request) -> Result<Response, Error> {
req.take_payload() let mut body = BytesMut::new();
.fold(BytesMut::new(), move |mut body, chunk| { while let Some(item) = req.payload().next().await {
body.extend_from_slice(&chunk); body.extend_from_slice(&item?)
Ok::<_, PayloadError>(body) }
})
.from_err() info!("request body: {:?}", body);
.and_then(|bytes| { Ok(Response::Ok()
info!("request body: {:?}", bytes); .header("x-head", HeaderValue::from_static("dummy value!"))
let mut res = Response::Ok(); .body(body))
res.header("x-head", HeaderValue::from_static("dummy value!"));
Ok(res.body(bytes))
})
} }
fn main() -> io::Result<()> { fn main() -> io::Result<()> {
@ -28,7 +25,7 @@ fn main() -> io::Result<()> {
Server::build() Server::build()
.bind("echo", "127.0.0.1:8080", || { .bind("echo", "127.0.0.1:8080", || {
HttpService::build().finish(|_req: Request| handle_request(_req)) HttpService::build().finish(handle_request).tcp()
})? })?
.run() .run()
} }

View File

@ -1,28 +0,0 @@
use std::{env, io};
use actix_codec::Framed;
use actix_http::{h1, Response, SendResponse, ServiceConfig};
use actix_server::{Io, Server};
use actix_service::{fn_service, NewService};
use actix_utils::framed::IntoFramed;
use actix_utils::stream::TakeItem;
use futures::Future;
use tokio_tcp::TcpStream;
fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "framed_hello=info");
env_logger::init();
Server::build()
.bind("framed_hello", "127.0.0.1:8080", || {
fn_service(|io: Io<TcpStream>| Ok(io.into_parts().0))
.and_then(IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())))
.and_then(TakeItem::new().map_err(|_| ()))
.and_then(|(_req, _framed): (_, Framed<_, _>)| {
SendResponse::send(_framed, Response::Ok().body("Hello world!"))
.map_err(|_| ())
.map(|_| ())
})
})?
.run()
}

View File

@ -21,6 +21,7 @@ fn main() -> io::Result<()> {
res.header("x-head", HeaderValue::from_static("dummy value!")); res.header("x-head", HeaderValue::from_static("dummy value!"));
future::ok::<_, ()>(res.body("Hello world!")) future::ok::<_, ()>(res.body("Hello world!"))
}) })
.tcp()
})? })?
.run() .run()
} }

View File

@ -1,8 +1,11 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, mem}; use std::{fmt, mem};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures::{Async, Poll, Stream}; use futures_core::Stream;
use pin_project::{pin_project, project};
use crate::error::Error; use crate::error::Error;
@ -30,31 +33,32 @@ impl BodySize {
/// Type that provides this trait can be streamed to a peer. /// Type that provides this trait can be streamed to a peer.
pub trait MessageBody { pub trait MessageBody {
fn length(&self) -> BodySize; fn size(&self) -> BodySize;
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error>; fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>>;
} }
impl MessageBody for () { impl MessageBody for () {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Empty BodySize::Empty
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, _: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
Ok(Async::Ready(None)) Poll::Ready(None)
} }
} }
impl<T: MessageBody> MessageBody for Box<T> { impl<T: MessageBody> MessageBody for Box<T> {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
self.as_ref().length() self.as_ref().size()
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
self.as_mut().poll_next() self.as_mut().poll_next(cx)
} }
} }
#[pin_project]
pub enum ResponseBody<B> { pub enum ResponseBody<B> {
Body(B), Body(B),
Other(Body), Other(Body),
@ -86,27 +90,34 @@ impl<B: MessageBody> ResponseBody<B> {
} }
impl<B: MessageBody> MessageBody for ResponseBody<B> { impl<B: MessageBody> MessageBody for ResponseBody<B> {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
ResponseBody::Body(ref body) => body.length(), ResponseBody::Body(ref body) => body.size(),
ResponseBody::Other(ref body) => body.length(), ResponseBody::Other(ref body) => body.size(),
} }
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
match self { match self {
ResponseBody::Body(ref mut body) => body.poll_next(), ResponseBody::Body(ref mut body) => body.poll_next(cx),
ResponseBody::Other(ref mut body) => body.poll_next(), ResponseBody::Other(ref mut body) => body.poll_next(cx),
} }
} }
} }
impl<B: MessageBody> Stream for ResponseBody<B> { impl<B: MessageBody> Stream for ResponseBody<B> {
type Item = Bytes; type Item = Result<Bytes, Error>;
type Error = Error;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { #[project]
self.poll_next() fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
#[project]
match self.project() {
ResponseBody::Body(ref mut body) => body.poll_next(cx),
ResponseBody::Other(ref mut body) => body.poll_next(cx),
}
} }
} }
@ -125,7 +136,7 @@ pub enum Body {
impl Body { impl Body {
/// Create body from slice (copy) /// Create body from slice (copy)
pub fn from_slice(s: &[u8]) -> Body { pub fn from_slice(s: &[u8]) -> Body {
Body::Bytes(Bytes::from(s)) Body::Bytes(Bytes::copy_from_slice(s))
} }
/// Create body from generic message body. /// Create body from generic message body.
@ -135,28 +146,28 @@ impl Body {
} }
impl MessageBody for Body { impl MessageBody for Body {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
Body::None => BodySize::None, Body::None => BodySize::None,
Body::Empty => BodySize::Empty, Body::Empty => BodySize::Empty,
Body::Bytes(ref bin) => BodySize::Sized(bin.len()), Body::Bytes(ref bin) => BodySize::Sized(bin.len()),
Body::Message(ref body) => body.length(), Body::Message(ref body) => body.size(),
} }
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
match self { match self {
Body::None => Ok(Async::Ready(None)), Body::None => Poll::Ready(None),
Body::Empty => Ok(Async::Ready(None)), Body::Empty => Poll::Ready(None),
Body::Bytes(ref mut bin) => { Body::Bytes(ref mut bin) => {
let len = bin.len(); let len = bin.len();
if len == 0 { if len == 0 {
Ok(Async::Ready(None)) Poll::Ready(None)
} else { } else {
Ok(Async::Ready(Some(bin.split_to(len)))) Poll::Ready(Some(Ok(mem::replace(bin, Bytes::new()))))
} }
} }
Body::Message(ref mut body) => body.poll_next(), Body::Message(ref mut body) => body.poll_next(cx),
} }
} }
} }
@ -182,10 +193,10 @@ impl PartialEq for Body {
} }
impl fmt::Debug for Body { impl fmt::Debug for Body {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self { match *self {
Body::None => write!(f, "Body::None"), Body::None => write!(f, "Body::None"),
Body::Empty => write!(f, "Body::Zero"), Body::Empty => write!(f, "Body::Empty"),
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b), Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b),
Body::Message(_) => write!(f, "Body::Message(_)"), Body::Message(_) => write!(f, "Body::Message(_)"),
} }
@ -218,7 +229,7 @@ impl From<String> for Body {
impl<'a> From<&'a String> for Body { impl<'a> From<&'a String> for Body {
fn from(s: &'a String) -> Body { fn from(s: &'a String) -> Body {
Body::Bytes(Bytes::from(AsRef::<[u8]>::as_ref(&s))) Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
} }
} }
@ -234,46 +245,69 @@ impl From<BytesMut> for Body {
} }
} }
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>> + 'static,
{
fn from(s: SizedStream<S>) -> Body {
Body::from_message(s)
}
}
impl<S, E> From<BodyStream<S, E>> for Body
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Error> + 'static,
{
fn from(s: BodyStream<S, E>) -> Body {
Body::from_message(s)
}
}
impl MessageBody for Bytes { impl MessageBody for Bytes {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len()) BodySize::Sized(self.len())
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, _: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() { if self.is_empty() {
Ok(Async::Ready(None)) Poll::Ready(None)
} else { } else {
Ok(Async::Ready(Some(mem::replace(self, Bytes::new())))) Poll::Ready(Some(Ok(mem::replace(self, Bytes::new()))))
} }
} }
} }
impl MessageBody for BytesMut { impl MessageBody for BytesMut {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len()) BodySize::Sized(self.len())
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, _: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() { if self.is_empty() {
Ok(Async::Ready(None)) Poll::Ready(None)
} else { } else {
Ok(Async::Ready(Some( Poll::Ready(Some(Ok(mem::replace(self, BytesMut::new()).freeze())))
mem::replace(self, BytesMut::new()).freeze(),
)))
} }
} }
} }
impl MessageBody for &'static str { impl MessageBody for &'static str {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len()) BodySize::Sized(self.len())
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, _: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() { if self.is_empty() {
Ok(Async::Ready(None)) Poll::Ready(None)
} else { } else {
Ok(Async::Ready(Some(Bytes::from_static( Poll::Ready(Some(Ok(Bytes::from_static(
mem::replace(self, "").as_ref(), mem::replace(self, "").as_ref(),
)))) ))))
} }
@ -281,48 +315,43 @@ impl MessageBody for &'static str {
} }
impl MessageBody for &'static [u8] { impl MessageBody for &'static [u8] {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len()) BodySize::Sized(self.len())
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, _: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() { if self.is_empty() {
Ok(Async::Ready(None)) Poll::Ready(None)
} else { } else {
Ok(Async::Ready(Some(Bytes::from_static(mem::replace( Poll::Ready(Some(Ok(Bytes::from_static(mem::replace(self, b"")))))
self, b"",
)))))
} }
} }
} }
impl MessageBody for Vec<u8> { impl MessageBody for Vec<u8> {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len()) BodySize::Sized(self.len())
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, _: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() { if self.is_empty() {
Ok(Async::Ready(None)) Poll::Ready(None)
} else { } else {
Ok(Async::Ready(Some(Bytes::from(mem::replace( Poll::Ready(Some(Ok(Bytes::from(mem::replace(self, Vec::new())))))
self,
Vec::new(),
)))))
} }
} }
} }
impl MessageBody for String { impl MessageBody for String {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len()) BodySize::Sized(self.len())
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, _: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() { if self.is_empty() {
Ok(Async::Ready(None)) Poll::Ready(None)
} else { } else {
Ok(Async::Ready(Some(Bytes::from( Poll::Ready(Some(Ok(Bytes::from(
mem::replace(self, String::new()).into_bytes(), mem::replace(self, String::new()).into_bytes(),
)))) ))))
} }
@ -331,14 +360,16 @@ impl MessageBody for String {
/// Type represent streaming body. /// Type represent streaming body.
/// Response does not contain `content-length` header and appropriate transfer encoding is used. /// Response does not contain `content-length` header and appropriate transfer encoding is used.
#[pin_project]
pub struct BodyStream<S, E> { pub struct BodyStream<S, E> {
#[pin]
stream: S, stream: S,
_t: PhantomData<E>, _t: PhantomData<E>,
} }
impl<S, E> BodyStream<S, E> impl<S, E> BodyStream<S, E>
where where
S: Stream<Item = Bytes, Error = E>, S: Stream<Item = Result<Bytes, E>>,
E: Into<Error>, E: Into<Error>,
{ {
pub fn new(stream: S) -> Self { pub fn new(stream: S) -> Self {
@ -351,50 +382,60 @@ where
impl<S, E> MessageBody for BodyStream<S, E> impl<S, E> MessageBody for BodyStream<S, E>
where where
S: Stream<Item = Bytes, Error = E>, S: Stream<Item = Result<Bytes, E>>,
E: Into<Error>, E: Into<Error>,
{ {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Stream BodySize::Stream
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
self.stream.poll().map_err(|e| e.into()) unsafe { Pin::new_unchecked(self) }
.project()
.stream
.poll_next(cx)
.map(|res| res.map(|res| res.map_err(std::convert::Into::into)))
} }
} }
/// Type represent streaming body. This body implementation should be used /// 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. /// if total size of stream is known. Data get sent as is without using transfer encoding.
#[pin_project]
pub struct SizedStream<S> { pub struct SizedStream<S> {
size: usize, size: u64,
#[pin]
stream: S, stream: S,
} }
impl<S> SizedStream<S> impl<S> SizedStream<S>
where where
S: Stream<Item = Bytes, Error = Error>, S: Stream<Item = Result<Bytes, Error>>,
{ {
pub fn new(size: usize, stream: S) -> Self { pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream } SizedStream { size, stream }
} }
} }
impl<S> MessageBody for SizedStream<S> impl<S> MessageBody for SizedStream<S>
where where
S: Stream<Item = Bytes, Error = Error>, S: Stream<Item = Result<Bytes, Error>>,
{ {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.size) BodySize::Sized64(self.size)
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
self.stream.poll() unsafe { Pin::new_unchecked(self) }
.project()
.stream
.poll_next(cx)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use futures_util::future::poll_fn;
impl Body { impl Body {
pub(crate) fn get_ref(&self) -> &[u8] { pub(crate) fn get_ref(&self) -> &[u8] {
@ -414,49 +455,138 @@ mod tests {
} }
} }
#[test] #[actix_rt::test]
fn test_static_str() { async fn test_static_str() {
assert_eq!(Body::from("").length(), BodySize::Sized(0)); assert_eq!(Body::from("").size(), BodySize::Sized(0));
assert_eq!(Body::from("test").length(), BodySize::Sized(4)); assert_eq!(Body::from("test").size(), BodySize::Sized(4));
assert_eq!(Body::from("test").get_ref(), b"test"); assert_eq!(Body::from("test").get_ref(), b"test");
assert_eq!("test".size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| "test".poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
} }
#[test] #[actix_rt::test]
fn test_static_bytes() { async fn test_static_bytes() {
assert_eq!(Body::from(b"test".as_ref()).length(), BodySize::Sized(4)); 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(b"test".as_ref()).get_ref(), b"test");
assert_eq!( assert_eq!(
Body::from_slice(b"test".as_ref()).length(), Body::from_slice(b"test".as_ref()).size(),
BodySize::Sized(4) BodySize::Sized(4)
); );
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test"); assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
assert_eq!((&b"test"[..]).size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| (&b"test"[..]).poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
} }
#[test] #[actix_rt::test]
fn test_vec() { async fn test_vec() {
assert_eq!(Body::from(Vec::from("test")).length(), BodySize::Sized(4)); assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test"); assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
assert_eq!(Vec::from("test").size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| Vec::from("test").poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
} }
#[test] #[actix_rt::test]
fn test_bytes() { async fn test_bytes() {
assert_eq!(Body::from(Bytes::from("test")).length(), BodySize::Sized(4)); let mut b = Bytes::from("test");
assert_eq!(Body::from(Bytes::from("test")).get_ref(), b"test"); assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
}
#[test]
fn test_string() {
let b = "test".to_owned();
assert_eq!(Body::from(b.clone()).length(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test"); assert_eq!(Body::from(b.clone()).get_ref(), b"test");
assert_eq!(Body::from(&b).length(), BodySize::Sized(4));
assert_eq!(Body::from(&b).get_ref(), b"test"); assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
} }
#[test] #[actix_rt::test]
fn test_bytes_mut() { async fn test_bytes_mut() {
let b = BytesMut::from("test"); let mut b = BytesMut::from("test");
assert_eq!(Body::from(b.clone()).length(), BodySize::Sized(4)); assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b).get_ref(), b"test"); assert_eq!(Body::from(b.clone()).get_ref(), b"test");
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_string() {
let mut 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");
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.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| ().poll_next(cx)).await.is_none());
}
#[actix_rt::test]
async fn test_box() {
let mut val = Box::new(());
assert_eq!(val.size(), BodySize::Empty);
assert!(poll_fn(|cx| val.poll_next(cx)).await.is_none());
}
#[actix_rt::test]
async fn test_body_eq() {
assert!(Body::None == Body::None);
assert!(Body::None != Body::Empty);
assert!(Body::Empty == Body::Empty);
assert!(Body::Empty != Body::None);
assert!(
Body::Bytes(Bytes::from_static(b"1"))
== Body::Bytes(Bytes::from_static(b"1"))
);
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)
);
} }
} }

View File

@ -1,53 +1,94 @@
use std::fmt::Debug;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::rc::Rc;
use std::{fmt, net};
use actix_server_config::ServerConfig as SrvConfig; use actix_codec::Framed;
use actix_service::{IntoNewService, NewService}; use actix_service::{IntoServiceFactory, Service, ServiceFactory};
use crate::body::MessageBody; use crate::body::MessageBody;
use crate::config::{KeepAlive, ServiceConfig}; use crate::config::{KeepAlive, ServiceConfig};
use crate::error::Error;
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler};
use crate::h2::H2Service;
use crate::helpers::{Data, DataFactory};
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::h1::H1Service;
use crate::h2::H2Service;
use crate::service::HttpService; use crate::service::HttpService;
/// A http service builder /// A http service builder
/// ///
/// This type can be used to construct an instance of `http service` through a /// This type can be used to construct an instance of `http service` through a
/// builder-like pattern. /// builder-like pattern.
pub struct HttpServiceBuilder<T, S> { pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_timeout: u64, client_timeout: u64,
client_disconnect: u64, client_disconnect: u64,
secure: bool,
local_addr: Option<net::SocketAddr>,
expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
_t: PhantomData<(T, S)>, _t: PhantomData<(T, S)>,
} }
impl<T, S> HttpServiceBuilder<T, S> impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler<T>>
where where
S: NewService<SrvConfig, Request = Request>, S: ServiceFactory<Config = (), Request = Request>,
S::Error: Debug + 'static, S::Error: Into<Error> + 'static,
S::Service: 'static, S::InitError: fmt::Debug,
<S::Service as Service>::Future: 'static,
{ {
/// Create instance of `ServiceConfigBuilder` /// Create instance of `ServiceConfigBuilder`
pub fn new() -> HttpServiceBuilder<T, S> { pub fn new() -> Self {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: KeepAlive::Timeout(5), keep_alive: KeepAlive::Timeout(5),
client_timeout: 5000, client_timeout: 5000,
client_disconnect: 0, client_disconnect: 0,
secure: false,
local_addr: None,
expect: ExpectHandler,
upgrade: None,
on_connect: None,
_t: PhantomData, _t: PhantomData,
} }
} }
}
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
where
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
<S::Service as Service>::Future: 'static,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service>::Future: 'static,
U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
<U::Service as Service>::Future: 'static,
{
/// Set server keep-alive setting. /// Set server keep-alive setting.
/// ///
/// By default keep alive is set to a 5 seconds. /// By default keep alive is set to a 5 seconds.
pub fn keep_alive<U: Into<KeepAlive>>(mut self, val: U) -> Self { pub fn keep_alive<W: Into<KeepAlive>>(mut self, val: W) -> Self {
self.keep_alive = val.into(); self.keep_alive = val.into();
self self
} }
/// Set connection secure state
pub fn secure(mut self) -> Self {
self.secure = true;
self
}
/// Set the local address that this service is bound to.
pub fn local_addr(mut self, addr: net::SocketAddr) -> Self {
self.local_addr = Some(addr);
self
}
/// Set server client timeout in milliseconds for first request. /// Set server client timeout in milliseconds for first request.
/// ///
/// Defines a timeout for reading client request header. If a client does not transmit /// Defines a timeout for reading client request header. If a client does not transmit
@ -75,67 +116,136 @@ where
self self
} }
// #[cfg(feature = "ssl")] /// Provide service for `EXPECT: 100-Continue` support.
// /// Configure alpn protocols for SslAcceptorBuilder. ///
// pub fn configure_openssl( /// Service get called with request that contains `EXPECT` header.
// builder: &mut openssl::ssl::SslAcceptorBuilder, /// Service must return request in case of success, in that case
// ) -> io::Result<()> { /// request will be forwarded to main service.
// let protos: &[u8] = b"\x02h2"; pub fn expect<F, X1>(self, expect: F) -> HttpServiceBuilder<T, S, X1, U>
// builder.set_alpn_select_callback(|_, protos| { where
// const H2: &[u8] = b"\x02h2"; F: IntoServiceFactory<X1>,
// if protos.windows(3).any(|window| window == H2) { X1: ServiceFactory<Config = (), Request = Request, Response = Request>,
// Ok(b"h2") X1::Error: Into<Error>,
// } else { X1::InitError: fmt::Debug,
// Err(openssl::ssl::AlpnError::NOACK) <X1::Service as Service>::Future: 'static,
// } {
// }); HttpServiceBuilder {
// builder.set_alpn_protos(&protos)?; keep_alive: self.keep_alive,
client_timeout: self.client_timeout,
client_disconnect: self.client_disconnect,
secure: self.secure,
local_addr: self.local_addr,
expect: expect.into_factory(),
upgrade: self.upgrade,
on_connect: self.on_connect,
_t: PhantomData,
}
}
// Ok(()) /// Provide service for custom `Connection: UPGRADE` support.
// } ///
/// If service is provided then normal requests handling get halted
/// and this service get called with original request and framed object.
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
where
F: IntoServiceFactory<U1>,
U1: ServiceFactory<
Config = (),
Request = (Request, Framed<T, Codec>),
Response = (),
>,
U1::Error: fmt::Display,
U1::InitError: fmt::Debug,
<U1::Service as Service>::Future: 'static,
{
HttpServiceBuilder {
keep_alive: self.keep_alive,
client_timeout: self.client_timeout,
client_disconnect: self.client_disconnect,
secure: self.secure,
local_addr: self.local_addr,
expect: self.expect,
upgrade: Some(upgrade.into_factory()),
on_connect: self.on_connect,
_t: PhantomData,
}
}
/// Set on-connect callback.
///
/// It get called once per connection and result of the call
/// get stored to the request's extensions.
pub fn on_connect<F, I>(mut self, f: F) -> Self
where
F: Fn(&T) -> I + 'static,
I: Clone + 'static,
{
self.on_connect = Some(Rc::new(move |io| Box::new(Data(f(io)))));
self
}
/// Finish service configuration and create *http service* for HTTP/1 protocol. /// Finish service configuration and create *http service* for HTTP/1 protocol.
pub fn h1<F, P, B>(self, service: F) -> H1Service<T, P, S, B> pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
where where
B: MessageBody + 'static, B: MessageBody,
F: IntoNewService<S, SrvConfig>, F: IntoServiceFactory<S>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
self.client_timeout, self.client_timeout,
self.client_disconnect, self.client_disconnect,
self.secure,
self.local_addr,
); );
H1Service::with_config(cfg, service.into_new_service()) H1Service::with_config(cfg, service.into_factory())
.expect(self.expect)
.upgrade(self.upgrade)
.on_connect(self.on_connect)
} }
/// Finish service configuration and create *http service* for HTTP/2 protocol. /// Finish service configuration and create *http service* for HTTP/2 protocol.
pub fn h2<F, P, B>(self, service: F) -> H2Service<T, P, S, B> pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where where
B: MessageBody + 'static, B: MessageBody + 'static,
F: IntoNewService<S, SrvConfig>, F: IntoServiceFactory<S>,
S::Response: Into<Response<B>>, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
self.client_timeout, self.client_timeout,
self.client_disconnect, self.client_disconnect,
self.secure,
self.local_addr,
); );
H2Service::with_config(cfg, service.into_new_service()) H2Service::with_config(cfg, service.into_factory()).on_connect(self.on_connect)
} }
/// Finish service configuration and create `HttpService` instance. /// Finish service configuration and create `HttpService` instance.
pub fn finish<F, P, B>(self, service: F) -> HttpService<T, P, S, B> pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
where where
B: MessageBody + 'static, B: MessageBody + 'static,
F: IntoNewService<S, SrvConfig>, F: IntoServiceFactory<S>,
S::Response: Into<Response<B>>, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
self.client_timeout, self.client_timeout,
self.client_disconnect, self.client_disconnect,
self.secure,
self.local_addr,
); );
HttpService::with_config(cfg, service.into_new_service()) HttpService::with_config(cfg, service.into_factory())
.expect(self.expect)
.upgrade(self.upgrade)
.on_connect(self.on_connect)
} }
} }

View File

@ -1,18 +1,20 @@
use std::{fmt, io, time}; use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, io, mem, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use bytes::{Buf, Bytes}; use bytes::{Buf, Bytes};
use futures::future::{err, Either, Future, FutureResult}; use futures_util::future::{err, Either, Future, FutureExt, LocalBoxFuture, Ready};
use futures::Poll;
use h2::client::SendRequest; use h2::client::SendRequest;
use pin_project::{pin_project, project};
use crate::body::MessageBody; use crate::body::MessageBody;
use crate::h1::ClientCodec; use crate::h1::ClientCodec;
use crate::message::{RequestHead, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; use crate::payload::Payload;
use super::error::SendRequestError; use super::error::SendRequestError;
use super::pool::Acquired; use super::pool::{Acquired, Protocol};
use super::{h1proto, h2proto}; use super::{h1proto, h2proto};
pub(crate) enum ConnectionType<Io> { pub(crate) enum ConnectionType<Io> {
@ -21,23 +23,24 @@ pub(crate) enum ConnectionType<Io> {
} }
pub trait Connection { pub trait Connection {
type Io: AsyncRead + AsyncWrite; type Io: AsyncRead + AsyncWrite + Unpin;
type Future: Future<Item = (ResponseHead, Payload), Error = SendRequestError>; type Future: Future<Output = Result<(ResponseHead, Payload), SendRequestError>>;
fn protocol(&self) -> Protocol;
/// Send request and body /// Send request and body
fn send_request<B: MessageBody + 'static>( fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
self, self,
head: RequestHead, head: H,
body: B, body: B,
) -> Self::Future; ) -> Self::Future;
type TunnelFuture: Future< type TunnelFuture: Future<
Item = (ResponseHead, Framed<Self::Io, ClientCodec>), Output = Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
Error = SendRequestError,
>; >;
/// Send request, returns Response and Framed /// Send request, returns Response and Framed
fn open_tunnel(self, head: RequestHead) -> Self::TunnelFuture; fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture;
} }
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static { pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
@ -60,7 +63,7 @@ impl<T> fmt::Debug for IoConnection<T>
where where
T: fmt::Debug, T: fmt::Debug,
{ {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.io { match self.io {
Some(ConnectionType::H1(ref io)) => write!(f, "H1Connection({:?})", io), Some(ConnectionType::H1(ref io)) => write!(f, "H1Connection({:?})", io),
Some(ConnectionType::H2(_)) => write!(f, "H2Connection"), Some(ConnectionType::H2(_)) => write!(f, "H2Connection"),
@ -69,7 +72,7 @@ where
} }
} }
impl<T: AsyncRead + AsyncWrite + 'static> IoConnection<T> { impl<T: AsyncRead + AsyncWrite + Unpin> IoConnection<T> {
pub(crate) fn new( pub(crate) fn new(
io: ConnectionType<T>, io: ConnectionType<T>,
created: time::Instant, created: time::Instant,
@ -89,49 +92,50 @@ impl<T: AsyncRead + AsyncWrite + 'static> IoConnection<T> {
impl<T> Connection for IoConnection<T> impl<T> Connection for IoConnection<T>
where where
T: AsyncRead + AsyncWrite + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
type Io = T; type Io = T;
type Future = Box<Future<Item = (ResponseHead, Payload), Error = SendRequestError>>; type Future =
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
fn send_request<B: MessageBody + 'static>( fn protocol(&self) -> Protocol {
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, mut self,
head: RequestHead, head: H,
body: B, body: B,
) -> Self::Future { ) -> Self::Future {
match self.io.take().unwrap() { match self.io.take().unwrap() {
ConnectionType::H1(io) => Box::new(h1proto::send_request( ConnectionType::H1(io) => {
io, h1proto::send_request(io, head.into(), body, self.created, self.pool)
head, .boxed_local()
body, }
self.created, ConnectionType::H2(io) => {
self.pool, h2proto::send_request(io, head.into(), body, self.created, self.pool)
)), .boxed_local()
ConnectionType::H2(io) => Box::new(h2proto::send_request( }
io,
head,
body,
self.created,
self.pool,
)),
} }
} }
type TunnelFuture = Either< type TunnelFuture = Either<
Box< LocalBoxFuture<
Future< 'static,
Item = (ResponseHead, Framed<Self::Io, ClientCodec>), Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
Error = SendRequestError,
>,
>, >,
FutureResult<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>, Ready<Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>>,
>; >;
/// Send request, returns Response and Framed /// Send request, returns Response and Framed
fn open_tunnel(mut self, head: RequestHead) -> Self::TunnelFuture { fn open_tunnel<H: Into<RequestHeadType>>(mut self, head: H) -> Self::TunnelFuture {
match self.io.take().unwrap() { match self.io.take().unwrap() {
ConnectionType::H1(io) => { ConnectionType::H1(io) => {
Either::A(Box::new(h1proto::open_tunnel(io, head))) Either::Left(h1proto::open_tunnel(io, head.into()).boxed_local())
} }
ConnectionType::H2(io) => { ConnectionType::H2(io) => {
if let Some(mut pool) = self.pool.take() { if let Some(mut pool) = self.pool.take() {
@ -141,7 +145,7 @@ where
None, None,
)); ));
} }
Either::B(err(SendRequestError::TunnelNotSupported)) Either::Right(err(SendRequestError::TunnelNotSupported))
} }
} }
} }
@ -155,15 +159,23 @@ pub(crate) enum EitherConnection<A, B> {
impl<A, B> Connection for EitherConnection<A, B> impl<A, B> Connection for EitherConnection<A, B>
where where
A: AsyncRead + AsyncWrite + 'static, A: AsyncRead + AsyncWrite + Unpin + 'static,
B: AsyncRead + AsyncWrite + 'static, B: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
type Io = EitherIo<A, B>; type Io = EitherIo<A, B>;
type Future = Box<Future<Item = (ResponseHead, Payload), Error = SendRequestError>>; type Future =
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
fn send_request<RB: MessageBody + 'static>( fn protocol(&self) -> Protocol {
match self {
EitherConnection::A(con) => con.protocol(),
EitherConnection::B(con) => con.protocol(),
}
}
fn send_request<RB: MessageBody + 'static, H: Into<RequestHeadType>>(
self, self,
head: RequestHead, head: H,
body: RB, body: RB,
) -> Self::Future { ) -> Self::Future {
match self { match self {
@ -172,44 +184,30 @@ where
} }
} }
type TunnelFuture = Box< type TunnelFuture = LocalBoxFuture<
Future< 'static,
Item = (ResponseHead, Framed<Self::Io, ClientCodec>), Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
Error = SendRequestError,
>,
>; >;
/// Send request, returns Response and Framed /// Send request, returns Response and Framed
fn open_tunnel(self, head: RequestHead) -> Self::TunnelFuture { fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture {
match self { match self {
EitherConnection::A(con) => Box::new( EitherConnection::A(con) => con
con.open_tunnel(head) .open_tunnel(head)
.map(|(head, framed)| (head, framed.map_io(|io| EitherIo::A(io)))), .map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::A))))
), .boxed_local(),
EitherConnection::B(con) => Box::new( EitherConnection::B(con) => con
con.open_tunnel(head) .open_tunnel(head)
.map(|(head, framed)| (head, framed.map_io(|io| EitherIo::B(io)))), .map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::B))))
), .boxed_local(),
} }
} }
} }
#[pin_project]
pub enum EitherIo<A, B> { pub enum EitherIo<A, B> {
A(A), A(#[pin] A),
B(B), B(#[pin] B),
}
impl<A, B> io::Read for EitherIo<A, B>
where
A: io::Read,
B: io::Read,
{
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
EitherIo::A(ref mut val) => val.read(buf),
EitherIo::B(ref mut val) => val.read(buf),
}
}
} }
impl<A, B> AsyncRead for EitherIo<A, B> impl<A, B> AsyncRead for EitherIo<A, B>
@ -217,7 +215,23 @@ where
A: AsyncRead, A: AsyncRead,
B: AsyncRead, B: AsyncRead,
{ {
unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { #[project]
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
#[project]
match self.project() {
EitherIo::A(val) => val.poll_read(cx, buf),
EitherIo::B(val) => val.poll_read(cx, buf),
}
}
unsafe fn prepare_uninitialized_buffer(
&self,
buf: &mut [mem::MaybeUninit<u8>],
) -> bool {
match self { match self {
EitherIo::A(ref val) => val.prepare_uninitialized_buffer(buf), EitherIo::A(ref val) => val.prepare_uninitialized_buffer(buf),
EitherIo::B(ref val) => val.prepare_uninitialized_buffer(buf), EitherIo::B(ref val) => val.prepare_uninitialized_buffer(buf),
@ -225,45 +239,58 @@ where
} }
} }
impl<A, B> io::Write for EitherIo<A, B>
where
A: io::Write,
B: io::Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
EitherIo::A(ref mut val) => val.write(buf),
EitherIo::B(ref mut val) => val.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
EitherIo::A(ref mut val) => val.flush(),
EitherIo::B(ref mut val) => val.flush(),
}
}
}
impl<A, B> AsyncWrite for EitherIo<A, B> impl<A, B> AsyncWrite for EitherIo<A, B>
where where
A: AsyncWrite, A: AsyncWrite,
B: AsyncWrite, B: AsyncWrite,
{ {
fn shutdown(&mut self) -> Poll<(), io::Error> { #[project]
match self { fn poll_write(
EitherIo::A(ref mut val) => val.shutdown(), self: Pin<&mut Self>,
EitherIo::B(ref mut val) => val.shutdown(), cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
#[project]
match self.project() {
EitherIo::A(val) => val.poll_write(cx, buf),
EitherIo::B(val) => val.poll_write(cx, buf),
} }
} }
fn write_buf<U: Buf>(&mut self, buf: &mut U) -> Poll<usize, io::Error> #[project]
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
#[project]
match self.project() {
EitherIo::A(val) => val.poll_flush(cx),
EitherIo::B(val) => val.poll_flush(cx),
}
}
#[project]
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
#[project]
match self.project() {
EitherIo::A(val) => val.poll_shutdown(cx),
EitherIo::B(val) => val.poll_shutdown(cx),
}
}
#[project]
fn poll_write_buf<U: Buf>(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut U,
) -> Poll<Result<usize, io::Error>>
where where
Self: Sized, Self: Sized,
{ {
match self { #[project]
EitherIo::A(ref mut val) => val.write_buf(buf), match self.project() {
EitherIo::B(ref mut val) => val.write_buf(buf), EitherIo::A(val) => val.poll_write_buf(cx, buf),
EitherIo::B(val) => val.poll_write_buf(cx, buf),
} }
} }
} }

View File

@ -6,23 +6,46 @@ use actix_codec::{AsyncRead, AsyncWrite};
use actix_connect::{ use actix_connect::{
default_connector, Connect as TcpConnect, Connection as TcpConnection, default_connector, Connect as TcpConnect, Connection as TcpConnection,
}; };
use actix_service::{apply_fn, Service, ServiceExt}; use actix_rt::net::TcpStream;
use actix_service::{apply_fn, Service};
use actix_utils::timeout::{TimeoutError, TimeoutService}; use actix_utils::timeout::{TimeoutError, TimeoutService};
use http::Uri; use http::Uri;
use tokio_tcp::TcpStream;
use super::connection::Connection; use super::connection::Connection;
use super::error::ConnectError; use super::error::ConnectError;
use super::pool::{ConnectionPool, Protocol}; use super::pool::{ConnectionPool, Protocol};
use super::Connect;
#[cfg(feature = "ssl")] #[cfg(feature = "openssl")]
use openssl::ssl::SslConnector; use actix_connect::ssl::openssl::SslConnector as OpensslConnector;
#[cfg(not(feature = "ssl"))] #[cfg(feature = "rustls")]
use actix_connect::ssl::rustls::ClientConfig;
#[cfg(feature = "rustls")]
use std::sync::Arc;
#[cfg(any(feature = "openssl", feature = "rustls"))]
enum SslConnector {
#[cfg(feature = "openssl")]
Openssl(OpensslConnector),
#[cfg(feature = "rustls")]
Rustls(Arc<ClientConfig>),
}
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
type SslConnector = (); type SslConnector = ();
/// Http client connector builde instance. /// Manages http client network connectivity
/// `Connector` type uses builder-like pattern for connector service construction. /// The `Connector` type uses a builder-like combinator pattern for service
/// construction that finishes by calling the `.finish()` method.
///
/// ```rust,ignore
/// use std::time::Duration;
/// use actix_http::client::Connector;
///
/// let connector = Connector::new()
/// .timeout(Duration::from_secs(5))
/// .finish();
/// ```
pub struct Connector<T, U> { pub struct Connector<T, U> {
connector: T, connector: T,
timeout: Duration, timeout: Duration,
@ -35,7 +58,11 @@ pub struct Connector<T, U> {
_t: PhantomData<U>, _t: PhantomData<U>,
} }
trait Io: AsyncRead + AsyncWrite + Unpin {}
impl<T: AsyncRead + AsyncWrite + Unpin> Io for T {}
impl Connector<(), ()> { impl Connector<(), ()> {
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
pub fn new() -> Connector< pub fn new() -> Connector<
impl Service< impl Service<
Request = TcpConnect<Uri>, Request = TcpConnect<Uri>,
@ -45,18 +72,27 @@ impl Connector<(), ()> {
TcpStream, TcpStream,
> { > {
let ssl = { let ssl = {
#[cfg(feature = "ssl")] #[cfg(feature = "openssl")]
{ {
use log::error; use actix_connect::ssl::openssl::SslMethod;
use openssl::ssl::{SslConnector, SslMethod};
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap();
let _ = ssl let _ = ssl
.set_alpn_protos(b"\x02h2\x08http/1.1") .set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| error!("Can not set alpn protocol: {:?}", e)); .map_err(|e| error!("Can not set alpn protocol: {:?}", e));
ssl.build() SslConnector::Openssl(ssl.build())
} }
#[cfg(not(feature = "ssl"))] #[cfg(all(not(feature = "openssl"), feature = "rustls"))]
{
let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let mut config = ClientConfig::new();
config.set_protocols(&protos);
config
.root_store
.add_server_trust_anchors(&actix_tls::rustls::TLS_SERVER_ROOTS);
SslConnector::Rustls(Arc::new(config))
}
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
{} {}
}; };
@ -77,7 +113,7 @@ impl<T, U> Connector<T, U> {
/// Use custom connector. /// Use custom connector.
pub fn connector<T1, U1>(self, connector: T1) -> Connector<T1, U1> pub fn connector<T1, U1>(self, connector: T1) -> Connector<T1, U1>
where where
U1: AsyncRead + AsyncWrite + fmt::Debug, U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug,
T1: Service< T1: Service<
Request = TcpConnect<Uri>, Request = TcpConnect<Uri>,
Response = TcpConnection<Uri, U1>, Response = TcpConnection<Uri, U1>,
@ -99,12 +135,13 @@ impl<T, U> Connector<T, U> {
impl<T, U> Connector<T, U> impl<T, U> Connector<T, U>
where where
U: AsyncRead + AsyncWrite + fmt::Debug + 'static, U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static,
T: Service< T: Service<
Request = TcpConnect<Uri>, Request = TcpConnect<Uri>,
Response = TcpConnection<Uri, U>, Response = TcpConnection<Uri, U>,
Error = actix_connect::ConnectError, Error = actix_connect::ConnectError,
> + Clone, > + Clone
+ 'static,
{ {
/// Connection timeout, i.e. max time to connect to remote host including dns name resolution. /// Connection timeout, i.e. max time to connect to remote host including dns name resolution.
/// Set to 1 second by default. /// Set to 1 second by default.
@ -113,10 +150,16 @@ where
self self
} }
#[cfg(feature = "ssl")] #[cfg(feature = "openssl")]
/// Use custom `SslConnector` instance. /// Use custom `SslConnector` instance.
pub fn ssl(mut self, connector: SslConnector) -> Self { pub fn ssl(mut self, connector: OpensslConnector) -> Self {
self.ssl = connector; self.ssl = SslConnector::Openssl(connector);
self
}
#[cfg(feature = "rustls")]
pub fn rustls(mut self, connector: Arc<ClientConfig>) -> Self {
self.ssl = SslConnector::Rustls(connector);
self self
} }
@ -164,17 +207,21 @@ where
} }
/// Finish configuration process and create connector service. /// Finish configuration process and create connector service.
pub fn service( /// The Connector builder always concludes by calling `finish()` last in
/// its combinator chain.
pub fn finish(
self, self,
) -> impl Service<Request = Uri, Response = impl Connection, Error = ConnectError> + Clone ) -> impl Service<Request = Connect, Response = impl Connection, Error = ConnectError>
{ + Clone {
#[cfg(not(feature = "ssl"))] #[cfg(not(any(feature = "openssl", feature = "rustls")))]
{ {
let connector = TimeoutService::new( let connector = TimeoutService::new(
self.timeout, self.timeout,
apply_fn(self.connector, |msg: Uri, srv| srv.call(msg.into())) apply_fn(self.connector, |msg: Connect, srv| {
.map_err(ConnectError::from) srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
.map(|stream| (stream.into_parts().0, Protocol::Http1)), })
.map_err(ConnectError::from)
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
) )
.map_err(|e| match e { .map_err(|e| match e {
TimeoutError::Service(e) => e, TimeoutError::Service(e) => e,
@ -191,33 +238,62 @@ where
), ),
} }
} }
#[cfg(feature = "ssl")] #[cfg(any(feature = "openssl", feature = "rustls"))]
{ {
const H2: &[u8] = b"h2"; const H2: &[u8] = b"h2";
use actix_connect::ssl::OpensslConnector; #[cfg(feature = "openssl")]
use actix_connect::ssl::openssl::OpensslConnector;
#[cfg(feature = "rustls")]
use actix_connect::ssl::rustls::{RustlsConnector, Session};
use actix_service::{boxed::service, pipeline};
let ssl_service = TimeoutService::new( let ssl_service = TimeoutService::new(
self.timeout, self.timeout,
apply_fn(self.connector.clone(), |msg: Uri, srv| srv.call(msg.into())) pipeline(
.map_err(ConnectError::from) apply_fn(self.connector.clone(), |msg: Connect, srv| {
.and_then( srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
OpensslConnector::service(self.ssl) })
.map_err(ConnectError::from) .map_err(ConnectError::from),
)
.and_then(match self.ssl {
#[cfg(feature = "openssl")]
SslConnector::Openssl(ssl) => service(
OpensslConnector::service(ssl)
.map(|stream| { .map(|stream| {
let sock = stream.into_parts().0; let sock = stream.into_parts().0;
let h2 = sock let h2 = sock
.get_ref()
.ssl() .ssl()
.selected_alpn_protocol() .selected_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2)) .map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false); .unwrap_or(false);
if h2 { if h2 {
(sock, Protocol::Http2) (Box::new(sock) as Box<dyn Io>, Protocol::Http2)
} else { } else {
(sock, Protocol::Http1) (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 { .map_err(|e| match e {
TimeoutError::Service(e) => e, TimeoutError::Service(e) => e,
@ -226,9 +302,11 @@ where
let tcp_service = TimeoutService::new( let tcp_service = TimeoutService::new(
self.timeout, self.timeout,
apply_fn(self.connector.clone(), |msg: Uri, srv| srv.call(msg.into())) apply_fn(self.connector, |msg: Connect, srv| {
.map_err(ConnectError::from) srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
.map(|stream| (stream.into_parts().0, Protocol::Http1)), })
.map_err(ConnectError::from)
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
) )
.map_err(|e| match e { .map_err(|e| match e {
TimeoutError::Service(e) => e, TimeoutError::Service(e) => e,
@ -255,27 +333,29 @@ where
} }
} }
#[cfg(not(feature = "ssl"))] #[cfg(not(any(feature = "openssl", feature = "rustls")))]
mod connect_impl { mod connect_impl {
use futures::future::{err, Either, FutureResult}; use std::task::{Context, Poll};
use futures::Poll;
use futures_util::future::{err, Either, Ready};
use super::*; use super::*;
use crate::client::connection::IoConnection; use crate::client::connection::IoConnection;
pub(crate) struct InnerConnector<T, Io> pub(crate) struct InnerConnector<T, Io>
where where
Io: AsyncRead + AsyncWrite + 'static, Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Uri, Response = (Io, Protocol), Error = ConnectError>, T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{ {
pub(crate) tcp_pool: ConnectionPool<T, Io>, pub(crate) tcp_pool: ConnectionPool<T, Io>,
} }
impl<T, Io> Clone for InnerConnector<T, Io> impl<T, Io> Clone for InnerConnector<T, Io>
where where
Io: AsyncRead + AsyncWrite + 'static, Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Uri, Response = (Io, Protocol), Error = ConnectError> T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ Clone, + 'static,
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
InnerConnector { InnerConnector {
@ -286,48 +366,52 @@ mod connect_impl {
impl<T, Io> Service for InnerConnector<T, Io> impl<T, Io> Service for InnerConnector<T, Io>
where where
Io: AsyncRead + AsyncWrite + 'static, Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Uri, Response = (Io, Protocol), Error = ConnectError>, T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{ {
type Request = Uri; type Request = Connect;
type Response = IoConnection<Io>; type Response = IoConnection<Io>;
type Error = ConnectError; type Error = ConnectError;
type Future = Either< type Future = Either<
<ConnectionPool<T, Io> as Service>::Future, <ConnectionPool<T, Io> as Service>::Future,
FutureResult<IoConnection<Io>, ConnectError>, Ready<Result<IoConnection<Io>, ConnectError>>,
>; >;
fn poll_ready(&mut self) -> Poll<(), Self::Error> { fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.tcp_pool.poll_ready() self.tcp_pool.poll_ready(cx)
} }
fn call(&mut self, req: Uri) -> Self::Future { fn call(&mut self, req: Connect) -> Self::Future {
match req.scheme_str() { match req.uri.scheme_str() {
Some("https") | Some("wss") => { Some("https") | Some("wss") => {
Either::B(err(ConnectError::SslIsNotSupported)) Either::Right(err(ConnectError::SslIsNotSupported))
} }
_ => Either::A(self.tcp_pool.call(req)), _ => Either::Left(self.tcp_pool.call(req)),
} }
} }
} }
} }
#[cfg(feature = "ssl")] #[cfg(any(feature = "openssl", feature = "rustls"))]
mod connect_impl { mod connect_impl {
use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use futures::future::{Either, FutureResult}; use futures_core::ready;
use futures::{Async, Future, Poll}; use futures_util::future::Either;
use super::*; use super::*;
use crate::client::connection::EitherConnection; use crate::client::connection::EitherConnection;
pub(crate) struct InnerConnector<T1, T2, Io1, Io2> pub(crate) struct InnerConnector<T1, T2, Io1, Io2>
where where
Io1: AsyncRead + AsyncWrite + 'static, Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Request = Uri, Response = (Io1, Protocol), Error = ConnectError>, T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>,
T2: Service<Request = Uri, Response = (Io2, Protocol), Error = ConnectError>, T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>,
{ {
pub(crate) tcp_pool: ConnectionPool<T1, Io1>, pub(crate) tcp_pool: ConnectionPool<T1, Io1>,
pub(crate) ssl_pool: ConnectionPool<T2, Io2>, pub(crate) ssl_pool: ConnectionPool<T2, Io2>,
@ -335,12 +419,12 @@ mod connect_impl {
impl<T1, T2, Io1, Io2> Clone for InnerConnector<T1, T2, Io1, Io2> impl<T1, T2, Io1, Io2> Clone for InnerConnector<T1, T2, Io1, Io2>
where where
Io1: AsyncRead + AsyncWrite + 'static, Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Request = Uri, Response = (Io1, Protocol), Error = ConnectError> T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ Clone, + 'static,
T2: Service<Request = Uri, Response = (Io2, Protocol), Error = ConnectError> T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone, + 'static,
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
InnerConnector { InnerConnector {
@ -352,91 +436,94 @@ mod connect_impl {
impl<T1, T2, Io1, Io2> Service for InnerConnector<T1, T2, Io1, Io2> impl<T1, T2, Io1, Io2> Service for InnerConnector<T1, T2, Io1, Io2>
where where
Io1: AsyncRead + AsyncWrite + 'static, Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Request = Uri, Response = (Io1, Protocol), Error = ConnectError>, T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
T2: Service<Request = Uri, Response = (Io2, Protocol), Error = ConnectError>, + 'static,
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
{ {
type Request = Uri; type Request = Connect;
type Response = EitherConnection<Io1, Io2>; type Response = EitherConnection<Io1, Io2>;
type Error = ConnectError; type Error = ConnectError;
type Future = Either< type Future = Either<
FutureResult<Self::Response, Self::Error>, InnerConnectorResponseA<T1, Io1, Io2>,
Either< InnerConnectorResponseB<T2, Io1, Io2>,
InnerConnectorResponseA<T1, Io1, Io2>,
InnerConnectorResponseB<T2, Io1, Io2>,
>,
>; >;
fn poll_ready(&mut self) -> Poll<(), Self::Error> { fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.tcp_pool.poll_ready() self.tcp_pool.poll_ready(cx)
} }
fn call(&mut self, req: Uri) -> Self::Future { fn call(&mut self, req: Connect) -> Self::Future {
match req.scheme_str() { match req.uri.scheme_str() {
Some("https") | Some("wss") => { Some("https") | Some("wss") => Either::Right(InnerConnectorResponseB {
Either::B(Either::B(InnerConnectorResponseB { fut: self.ssl_pool.call(req),
fut: self.ssl_pool.call(req), _t: PhantomData,
_t: PhantomData, }),
})) _ => Either::Left(InnerConnectorResponseA {
}
_ => Either::B(Either::A(InnerConnectorResponseA {
fut: self.tcp_pool.call(req), fut: self.tcp_pool.call(req),
_t: PhantomData, _t: PhantomData,
})), }),
} }
} }
} }
#[pin_project::pin_project]
pub(crate) struct InnerConnectorResponseA<T, Io1, Io2> pub(crate) struct InnerConnectorResponseA<T, Io1, Io2>
where where
Io1: AsyncRead + AsyncWrite + 'static, Io1: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Uri, Response = (Io1, Protocol), Error = ConnectError>, T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ 'static,
{ {
#[pin]
fut: <ConnectionPool<T, Io1> as Service>::Future, fut: <ConnectionPool<T, Io1> as Service>::Future,
_t: PhantomData<Io2>, _t: PhantomData<Io2>,
} }
impl<T, Io1, Io2> Future for InnerConnectorResponseA<T, Io1, Io2> impl<T, Io1, Io2> Future for InnerConnectorResponseA<T, Io1, Io2>
where where
T: Service<Request = Uri, Response = (Io1, Protocol), Error = ConnectError>, T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
Io1: AsyncRead + AsyncWrite + 'static, + 'static,
Io2: AsyncRead + AsyncWrite + 'static, Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
type Item = EitherConnection<Io1, Io2>; type Output = Result<EitherConnection<Io1, Io2>, ConnectError>;
type Error = ConnectError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.fut.poll()? { Poll::Ready(
Async::NotReady => Ok(Async::NotReady), ready!(Pin::new(&mut self.get_mut().fut).poll(cx))
Async::Ready(res) => Ok(Async::Ready(EitherConnection::A(res))), .map(EitherConnection::A),
} )
} }
} }
#[pin_project::pin_project]
pub(crate) struct InnerConnectorResponseB<T, Io1, Io2> pub(crate) struct InnerConnectorResponseB<T, Io1, Io2>
where where
Io2: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Uri, Response = (Io2, Protocol), Error = ConnectError>, T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
{ {
#[pin]
fut: <ConnectionPool<T, Io2> as Service>::Future, fut: <ConnectionPool<T, Io2> as Service>::Future,
_t: PhantomData<Io1>, _t: PhantomData<Io1>,
} }
impl<T, Io1, Io2> Future for InnerConnectorResponseB<T, Io1, Io2> impl<T, Io1, Io2> Future for InnerConnectorResponseB<T, Io1, Io2>
where where
T: Service<Request = Uri, Response = (Io2, Protocol), Error = ConnectError>, T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
Io1: AsyncRead + AsyncWrite + 'static, + 'static,
Io2: AsyncRead + AsyncWrite + 'static, Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
type Item = EitherConnection<Io1, Io2>; type Output = Result<EitherConnection<Io1, Io2>, ConnectError>;
type Error = ConnectError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.fut.poll()? { Poll::Ready(
Async::NotReady => Ok(Async::NotReady), ready!(Pin::new(&mut self.get_mut().fut).poll(cx))
Async::Ready(res) => Ok(Async::Ready(EitherConnection::B(res))), .map(EitherConnection::B),
} )
} }
} }
} }

View File

@ -1,14 +1,13 @@
use std::io; use std::io;
use actix_connect::resolver::ResolveError;
use derive_more::{Display, From}; use derive_more::{Display, From};
use trust_dns_resolver::error::ResolveError;
#[cfg(feature = "ssl")] #[cfg(feature = "openssl")]
use openssl::ssl::{Error as SslError, HandshakeError}; use actix_connect::ssl::openssl::{HandshakeError, SslError};
use crate::error::{Error, ParseError, ResponseError}; use crate::error::{Error, ParseError, ResponseError};
use crate::http::Error as HttpError; use crate::http::{Error as HttpError, StatusCode};
use crate::response::Response;
/// 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)]
@ -18,10 +17,15 @@ pub enum ConnectError {
SslIsNotSupported, SslIsNotSupported,
/// SSL error /// SSL error
#[cfg(feature = "ssl")] #[cfg(feature = "openssl")]
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
SslError(SslError), SslError(SslError),
/// SSL Handshake error
#[cfg(feature = "openssl")]
#[display(fmt = "{}", _0)]
SslHandshakeError(String),
/// 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(ResolveError),
@ -63,14 +67,10 @@ impl From<actix_connect::ConnectError> for ConnectError {
} }
} }
#[cfg(feature = "ssl")] #[cfg(feature = "openssl")]
impl<T> From<HandshakeError<T>> for ConnectError { impl<T: std::fmt::Debug> From<HandshakeError<T>> for ConnectError {
fn from(err: HandshakeError<T>) -> ConnectError { fn from(err: HandshakeError<T>) -> ConnectError {
match err { ConnectError::SslHandshakeError(format!("{:?}", err))
HandshakeError::SetupFailure(stack) => SslError::from(stack).into(),
HandshakeError::Failure(stream) => stream.into_error().into(),
HandshakeError::WouldBlock(stream) => stream.into_error().into(),
}
} }
} }
@ -105,6 +105,9 @@ pub enum SendRequestError {
/// Http2 error /// Http2 error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
H2(h2::Error), H2(h2::Error),
/// Response took too long
#[display(fmt = "Timeout out while waiting for response")]
Timeout,
/// Tunnels are not supported for http2 connection /// Tunnels are not supported for http2 connection
#[display(fmt = "Tunnels are not supported for http2 connection")] #[display(fmt = "Tunnels are not supported for http2 connection")]
TunnelNotSupported, TunnelNotSupported,
@ -114,14 +117,33 @@ pub enum SendRequestError {
/// Convert `SendRequestError` to a server `Response` /// Convert `SendRequestError` to a server `Response`
impl ResponseError for SendRequestError { impl ResponseError for SendRequestError {
fn error_response(&self) -> Response { fn status_code(&self) -> StatusCode {
match *self { match *self {
SendRequestError::Connect(ConnectError::Timeout) => { SendRequestError::Connect(ConnectError::Timeout) => {
Response::GatewayTimeout() StatusCode::GATEWAY_TIMEOUT
} }
SendRequestError::Connect(_) => Response::BadGateway(), SendRequestError::Connect(_) => StatusCode::BAD_REQUEST,
_ => Response::InternalServerError(), _ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
/// A set of errors that can occur during freezing a request
#[derive(Debug, Display, From)]
pub enum FreezeRequestError {
/// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl),
/// Http error
#[display(fmt = "{}", _0)]
Http(HttpError),
}
impl From<FreezeRequestError> for SendRequestError {
fn from(e: FreezeRequestError) -> Self {
match e {
FreezeRequestError::Url(e) => e.into(),
FreezeRequestError::Http(e) => e.into(),
} }
.into()
} }
} }

View File

@ -1,13 +1,20 @@
use std::{io, time}; use std::io::Write;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{io, mem, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use bytes::Bytes; use bytes::buf::BufMutExt;
use futures::future::{ok, Either}; use bytes::{Bytes, BytesMut};
use futures::{Async, Future, Poll, Sink, Stream}; use futures_core::Stream;
use futures_util::future::poll_fn;
use futures_util::{SinkExt, StreamExt};
use crate::error::PayloadError; use crate::error::PayloadError;
use crate::h1; use crate::h1;
use crate::message::{RequestHead, ResponseHead}; use crate::header::HeaderMap;
use crate::http::header::{IntoHeaderValue, HOST};
use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::{Payload, PayloadStream}; use crate::payload::{Payload, PayloadStream};
use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; use super::connection::{ConnectionLifetime, ConnectionType, IoConnection};
@ -15,85 +22,143 @@ use super::error::{ConnectError, SendRequestError};
use super::pool::Acquired; use super::pool::Acquired;
use crate::body::{BodySize, MessageBody}; use crate::body::{BodySize, MessageBody};
pub(crate) fn send_request<T, B>( pub(crate) async fn send_request<T, B>(
io: T, io: T,
head: RequestHead, mut head: RequestHeadType,
body: B, body: B,
created: time::Instant, created: time::Instant,
pool: Option<Acquired<T>>, pool: Option<Acquired<T>>,
) -> impl Future<Item = (ResponseHead, Payload), Error = SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
T: AsyncRead + AsyncWrite + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
B: MessageBody, B: MessageBody,
{ {
// set request host header
if !head.as_ref().headers.contains_key(HOST)
&& !head.extra_headers().iter().any(|h| h.contains_key(HOST))
{
if let Some(host) = head.as_ref().uri.host() {
let mut wrt = BytesMut::with_capacity(host.len() + 5).writer();
let _ = match head.as_ref().uri.port_u16() {
None | Some(80) | Some(443) => write!(wrt, "{}", host),
Some(port) => write!(wrt, "{}:{}", host, port),
};
match wrt.get_mut().split().freeze().try_into() {
Ok(value) => match head {
RequestHeadType::Owned(ref mut head) => {
head.headers.insert(HOST, value)
}
RequestHeadType::Rc(_, ref mut extra_headers) => {
let headers = extra_headers.get_or_insert(HeaderMap::new());
headers.insert(HOST, value)
}
},
Err(e) => log::error!("Can not set HOST header {}", e),
}
}
}
let io = H1Connection { let io = H1Connection {
created, created,
pool, pool,
io: Some(io), io: Some(io),
}; };
let len = body.length(); // create Framed and send request
let mut framed = Framed::new(io, h1::ClientCodec::default());
framed.send((head, body.size()).into()).await?;
// create Framed and send reqest // send request body
Framed::new(io, h1::ClientCodec::default()) match body.size() {
.send((head, len).into()) BodySize::None | BodySize::Empty | BodySize::Sized(0) => (),
.from_err() _ => send_body(body, &mut framed).await?,
// send request body };
.and_then(move |framed| match body.length() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => { // read response and init read body
Either::A(ok(framed)) let res = framed.into_future().await;
} let (head, framed) = if let (Some(result), framed) = res {
_ => Either::B(SendBody::new(body, framed)), let item = result.map_err(SendRequestError::from)?;
}) (item, framed)
// read response and init read body } else {
.and_then(|framed| { return Err(SendRequestError::from(ConnectError::Disconnected));
framed };
.into_future()
.map_err(|(e, _)| SendRequestError::from(e)) match framed.get_codec().message_type() {
.and_then(|(item, framed)| { h1::MessageType::None => {
if let Some(res) = item { let force_close = !framed.get_codec().keepalive();
match framed.get_codec().message_type() { release_connection(framed, force_close);
h1::MessageType::None => { Ok((head, Payload::None))
let force_close = !framed.get_codec().keepalive(); }
release_connection(framed, force_close); _ => {
Ok((res, Payload::None)) let pl: PayloadStream = PlStream::new(framed).boxed_local();
} Ok((head, pl.into()))
_ => { }
let pl: PayloadStream = Box::new(PlStream::new(framed)); }
Ok((res, pl.into()))
}
}
} else {
Err(ConnectError::Disconnected.into())
}
})
})
} }
pub(crate) fn open_tunnel<T>( pub(crate) async fn open_tunnel<T>(
io: T, io: T,
head: RequestHead, head: RequestHeadType,
) -> impl Future<Item = (ResponseHead, Framed<T, h1::ClientCodec>), Error = SendRequestError> ) -> Result<(ResponseHead, Framed<T, h1::ClientCodec>), SendRequestError>
where where
T: AsyncRead + AsyncWrite + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
// create Framed and send reqest // create Framed and send request
Framed::new(io, h1::ClientCodec::default()) let mut framed = Framed::new(io, h1::ClientCodec::default());
.send((head, BodySize::None).into()) framed.send((head, BodySize::None).into()).await?;
.from_err()
// read response // read response
.and_then(|framed| { if let (Some(result), framed) = framed.into_future().await {
framed let head = result.map_err(SendRequestError::from)?;
.into_future() Ok((head, framed))
.map_err(|(e, _)| SendRequestError::from(e)) } else {
.and_then(|(head, framed)| { Err(SendRequestError::from(ConnectError::Disconnected))
if let Some(head) = head { }
Ok((head, framed)) }
/// send request body to the peer
pub(crate) async fn send_body<I, B>(
mut body: B,
framed: &mut Framed<I, h1::ClientCodec>,
) -> Result<(), SendRequestError>
where
I: ConnectionLifetime,
B: MessageBody,
{
let mut eof = false;
while !eof {
while !eof && !framed.is_write_buf_full() {
match poll_fn(|cx| body.poll_next(cx)).await {
Some(result) => {
framed.write(h1::Message::Chunk(Some(result?)))?;
}
None => {
eof = true;
framed.write(h1::Message::Chunk(None))?;
}
}
}
if !framed.is_write_buf_empty() {
poll_fn(|cx| match framed.flush(cx) {
Poll::Ready(Ok(_)) => Poll::Ready(Ok(())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => {
if !framed.is_write_buf_full() {
Poll::Ready(Ok(()))
} else { } else {
Err(SendRequestError::from(ConnectError::Disconnected)) Poll::Pending
} }
}) }
}) })
.await?;
}
}
SinkExt::flush(framed).await?;
Ok(())
} }
#[doc(hidden)] #[doc(hidden)]
@ -104,7 +169,10 @@ pub struct H1Connection<T> {
pool: Option<Acquired<T>>, pool: Option<Acquired<T>>,
} }
impl<T: AsyncRead + AsyncWrite + 'static> ConnectionLifetime for H1Connection<T> { impl<T> ConnectionLifetime for H1Connection<T>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
{
/// Close connection /// Close connection
fn close(&mut self) { fn close(&mut self) {
if let Some(mut pool) = self.pool.take() { if let Some(mut pool) = self.pool.take() {
@ -132,98 +200,44 @@ impl<T: AsyncRead + AsyncWrite + 'static> ConnectionLifetime for H1Connection<T>
} }
} }
impl<T: AsyncRead + AsyncWrite + 'static> io::Read for H1Connection<T> { impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncRead for H1Connection<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { unsafe fn prepare_uninitialized_buffer(
self.io.as_mut().unwrap().read(buf) &self,
buf: &mut [mem::MaybeUninit<u8>],
) -> bool {
self.io.as_ref().unwrap().prepare_uninitialized_buffer(buf)
}
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
} }
} }
impl<T: AsyncRead + AsyncWrite + 'static> AsyncRead for H1Connection<T> {} impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncWrite for H1Connection<T> {
fn poll_write(
impl<T: AsyncRead + AsyncWrite + 'static> io::Write for H1Connection<T> { mut self: Pin<&mut Self>,
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { cx: &mut Context<'_>,
self.io.as_mut().unwrap().write(buf) buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_write(cx, buf)
} }
fn flush(&mut self) -> io::Result<()> { fn poll_flush(
self.io.as_mut().unwrap().flush() mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
Pin::new(self.io.as_mut().unwrap()).poll_flush(cx)
} }
}
impl<T: AsyncRead + AsyncWrite + 'static> AsyncWrite for H1Connection<T> { fn poll_shutdown(
fn shutdown(&mut self) -> Poll<(), io::Error> { mut self: Pin<&mut Self>,
self.io.as_mut().unwrap().shutdown() cx: &mut Context<'_>,
} ) -> Poll<Result<(), io::Error>> {
} Pin::new(self.io.as_mut().unwrap()).poll_shutdown(cx)
/// Future responsible for sending request body to the peer
pub(crate) struct SendBody<I, B> {
body: Option<B>,
framed: Option<Framed<I, h1::ClientCodec>>,
flushed: bool,
}
impl<I, B> SendBody<I, B>
where
I: AsyncRead + AsyncWrite + 'static,
B: MessageBody,
{
pub(crate) fn new(body: B, framed: Framed<I, h1::ClientCodec>) -> Self {
SendBody {
body: Some(body),
framed: Some(framed),
flushed: true,
}
}
}
impl<I, B> Future for SendBody<I, B>
where
I: ConnectionLifetime,
B: MessageBody,
{
type Item = Framed<I, h1::ClientCodec>;
type Error = SendRequestError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let mut body_ready = true;
loop {
while body_ready
&& self.body.is_some()
&& !self.framed.as_ref().unwrap().is_write_buf_full()
{
match self.body.as_mut().unwrap().poll_next()? {
Async::Ready(item) => {
// check if body is done
if item.is_none() {
let _ = self.body.take();
}
self.flushed = false;
self.framed
.as_mut()
.unwrap()
.force_send(h1::Message::Chunk(item))?;
break;
}
Async::NotReady => body_ready = false,
}
}
if !self.flushed {
match self.framed.as_mut().unwrap().poll_complete()? {
Async::Ready(_) => {
self.flushed = true;
continue;
}
Async::NotReady => return Ok(Async::NotReady),
}
}
if self.body.is_none() {
return Ok(Async::Ready(self.framed.take().unwrap()));
}
return Ok(Async::NotReady);
}
} }
} }
@ -240,23 +254,27 @@ impl<Io: ConnectionLifetime> PlStream<Io> {
} }
impl<Io: ConnectionLifetime> Stream for PlStream<Io> { impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
type Item = Bytes; type Item = Result<Bytes, PayloadError>;
type Error = PayloadError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { fn poll_next(
match self.framed.as_mut().unwrap().poll()? { self: Pin<&mut Self>,
Async::NotReady => Ok(Async::NotReady), cx: &mut Context<'_>,
Async::Ready(Some(chunk)) => { ) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
match this.framed.as_mut().unwrap().next_item(cx)? {
Poll::Pending => Poll::Pending,
Poll::Ready(Some(chunk)) => {
if let Some(chunk) = chunk { if let Some(chunk) = chunk {
Ok(Async::Ready(Some(chunk))) Poll::Ready(Some(Ok(chunk)))
} else { } else {
let framed = self.framed.take().unwrap(); let framed = this.framed.take().unwrap();
let force_close = framed.get_codec().keepalive(); let force_close = !framed.get_codec().keepalive();
release_connection(framed, force_close); release_connection(framed, force_close);
Ok(Async::Ready(None)) Poll::Ready(None)
} }
} }
Async::Ready(None) => Ok(Async::Ready(None)), Poll::Ready(None) => Poll::Ready(None),
} }
} }
} }

View File

@ -1,175 +1,175 @@
use std::convert::TryFrom;
use std::time; use std::time;
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use bytes::Bytes; use bytes::Bytes;
use futures::future::{err, Either}; use futures_util::future::poll_fn;
use futures::{Async, Future, Poll};
use h2::{client::SendRequest, SendStream}; use h2::{client::SendRequest, SendStream};
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING};
use http::{request::Request, HttpTryFrom, Method, Version}; use http::{request::Request, Method, Version};
use crate::body::{BodySize, MessageBody}; use crate::body::{BodySize, MessageBody};
use crate::message::{RequestHead, ResponseHead}; use crate::header::HeaderMap;
use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; use crate::payload::Payload;
use super::connection::{ConnectionType, IoConnection}; use super::connection::{ConnectionType, IoConnection};
use super::error::SendRequestError; use super::error::SendRequestError;
use super::pool::Acquired; use super::pool::Acquired;
pub(crate) fn send_request<T, B>( pub(crate) async fn send_request<T, B>(
io: SendRequest<Bytes>, mut io: SendRequest<Bytes>,
head: RequestHead, head: RequestHeadType,
body: B, body: B,
created: time::Instant, created: time::Instant,
pool: Option<Acquired<T>>, pool: Option<Acquired<T>>,
) -> impl Future<Item = (ResponseHead, Payload), Error = SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
T: AsyncRead + AsyncWrite + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
B: MessageBody, B: MessageBody,
{ {
trace!("Sending client request: {:?} {:?}", head, body.length()); trace!("Sending client request: {:?} {:?}", head, body.size());
let head_req = head.method == Method::HEAD; let head_req = head.as_ref().method == Method::HEAD;
let length = body.length(); let length = body.size();
let eof = match length { let eof = match length {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => true, BodySize::None | BodySize::Empty | BodySize::Sized(0) => true,
_ => false, _ => false,
}; };
io.ready() let mut req = Request::new(());
.map_err(SendRequestError::from) *req.uri_mut() = head.as_ref().uri.clone();
.and_then(move |mut io| { *req.method_mut() = head.as_ref().method.clone();
let mut req = Request::new(()); *req.version_mut() = Version::HTTP_2;
*req.uri_mut() = head.uri;
*req.method_mut() = head.method;
*req.version_mut() = Version::HTTP_2;
let mut skip_len = true; let mut skip_len = true;
// let mut has_date = false; // let mut has_date = false;
// Content length // Content length
let _ = match length { let _ = match length {
BodySize::None => None, BodySize::None => None,
BodySize::Stream => { BodySize::Stream => {
skip_len = false; skip_len = false;
None None
} }
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) => req.headers_mut().insert(
CONTENT_LENGTH, CONTENT_LENGTH,
HeaderValue::try_from(format!("{}", len)).unwrap(), HeaderValue::try_from(format!("{}", len)).unwrap(),
), ),
BodySize::Sized64(len) => req.headers_mut().insert( BodySize::Sized64(len) => req.headers_mut().insert(
CONTENT_LENGTH, CONTENT_LENGTH,
HeaderValue::try_from(format!("{}", len)).unwrap(), HeaderValue::try_from(format!("{}", len)).unwrap(),
), ),
}; };
// copy headers // Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate.
for (key, value) in head.headers.iter() { let (head, extra_headers) = match head {
match *key { RequestHeadType::Owned(head) => (RequestHeadType::Owned(head), HeaderMap::new()),
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific RequestHeadType::Rc(head, extra_headers) => (
CONTENT_LENGTH if skip_len => continue, RequestHeadType::Rc(head, None),
// DATE => has_date = true, extra_headers.unwrap_or_else(HeaderMap::new),
_ => (), ),
} };
req.headers_mut().append(key, value.clone());
// merging headers from head and extra headers.
let headers = head
.as_ref()
.headers
.iter()
.filter(|(name, _)| !extra_headers.contains_key(*name))
.chain(extra_headers.iter());
// copy headers
for (key, value) in headers {
match *key {
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
CONTENT_LENGTH if skip_len => continue,
// DATE => has_date = true,
_ => (),
}
req.headers_mut().append(key, value.clone());
}
let res = poll_fn(|cx| io.poll_ready(cx)).await;
if let Err(e) = res {
release(io, pool, created, e.is_io());
return Err(SendRequestError::from(e));
}
let resp = match io.send_request(req, eof) {
Ok((fut, send)) => {
release(io, pool, created, false);
if !eof {
send_body(body, send).await?;
} }
fut.await.map_err(SendRequestError::from)?
}
Err(e) => {
release(io, pool, created, e.is_io());
return Err(e.into());
}
};
match io.send_request(req, eof) { let (parts, body) = resp.into_parts();
Ok((res, send)) => { let payload = if head_req { Payload::None } else { body.into() };
release(io, pool, created, false);
if !eof { let mut head = ResponseHead::new(parts.status);
Either::A(Either::B( head.version = parts.version;
SendBody { head.headers = parts.headers.into();
body, Ok((head, payload))
send,
buf: None,
}
.and_then(move |_| res.map_err(SendRequestError::from)),
))
} else {
Either::B(res.map_err(SendRequestError::from))
}
}
Err(e) => {
release(io, pool, created, e.is_io());
Either::A(Either::A(err(e.into())))
}
}
})
.and_then(move |resp| {
let (parts, body) = resp.into_parts();
let payload = if head_req { Payload::None } else { body.into() };
let mut head = ResponseHead::default();
head.version = parts.version;
head.status = parts.status;
head.headers = parts.headers;
Ok((head, payload))
})
.from_err()
} }
struct SendBody<B: MessageBody> { async fn send_body<B: MessageBody>(
body: B, mut body: B,
send: SendStream<Bytes>, mut send: SendStream<Bytes>,
buf: Option<Bytes>, ) -> Result<(), SendRequestError> {
} let mut buf = None;
loop {
impl<B: MessageBody> Future for SendBody<B> { if buf.is_none() {
type Item = (); match poll_fn(|cx| body.poll_next(cx)).await {
type Error = SendRequestError; Some(Ok(b)) => {
send.reserve_capacity(b.len());
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { buf = Some(b);
loop {
if self.buf.is_none() {
match self.body.poll_next() {
Ok(Async::Ready(Some(buf))) => {
self.send.reserve_capacity(buf.len());
self.buf = Some(buf);
}
Ok(Async::Ready(None)) => {
if let Err(e) = self.send.send_data(Bytes::new(), true) {
return Err(e.into());
}
self.send.reserve_capacity(0);
return Ok(Async::Ready(()));
}
Ok(Async::NotReady) => return Ok(Async::NotReady),
Err(e) => return Err(e.into()),
} }
} Some(Err(e)) => return Err(e.into()),
None => {
match self.send.poll_capacity() { if let Err(e) = send.send_data(Bytes::new(), true) {
Ok(Async::NotReady) => return Ok(Async::NotReady),
Ok(Async::Ready(None)) => return Ok(Async::Ready(())),
Ok(Async::Ready(Some(cap))) => {
let mut buf = self.buf.take().unwrap();
let len = buf.len();
let bytes = buf.split_to(std::cmp::min(cap, len));
if let Err(e) = self.send.send_data(bytes, false) {
return Err(e.into()); return Err(e.into());
} else {
if !buf.is_empty() {
self.send.reserve_capacity(buf.len());
self.buf = Some(buf);
}
continue;
} }
send.reserve_capacity(0);
return Ok(());
} }
Err(e) => return Err(e.into()),
} }
} }
match poll_fn(|cx| send.poll_capacity(cx)).await {
None => return Ok(()),
Some(Ok(cap)) => {
let b = buf.as_mut().unwrap();
let len = b.len();
let bytes = b.split_to(std::cmp::min(cap, len));
if let Err(e) = send.send_data(bytes, false) {
return Err(e.into());
} else {
if !b.is_empty() {
send.reserve_capacity(b.len());
} else {
buf = None;
}
continue;
}
}
Some(Err(e)) => return Err(e.into()),
}
} }
} }
// release SendRequest object // release SendRequest object
fn release<T: AsyncRead + AsyncWrite + 'static>( fn release<T: AsyncRead + AsyncWrite + Unpin + 'static>(
io: SendRequest<Bytes>, io: SendRequest<Bytes>,
pool: Option<Acquired<T>>, pool: Option<Acquired<T>>,
created: time::Instant, created: time::Instant,

View File

@ -1,4 +1,6 @@
//! Http client api //! Http client api
use http::Uri;
mod connection; mod connection;
mod connector; mod connector;
mod error; mod error;
@ -8,4 +10,11 @@ mod pool;
pub use self::connection::Connection; pub use self::connection::Connection;
pub use self::connector::Connector; pub use self::connector::Connector;
pub use self::error::{ConnectError, InvalidUrl, SendRequestError}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
pub use self::pool::Protocol;
#[derive(Clone)]
pub struct Connect {
pub uri: Uri,
pub addr: Option<std::net::SocketAddr>,
}

View File

@ -1,27 +1,29 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::io; use std::future::Future;
use std::pin::Pin;
use std::rc::Rc; use std::rc::Rc;
use std::task::{Context, Poll};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{delay_for, Delay};
use actix_service::Service; use actix_service::Service;
use actix_utils::{oneshot, task::LocalWaker};
use bytes::Bytes; use bytes::Bytes;
use futures::future::{err, ok, Either, FutureResult}; use futures_util::future::{poll_fn, FutureExt, LocalBoxFuture};
use futures::task::AtomicTask; use fxhash::FxHashMap;
use futures::unsync::oneshot; use h2::client::{handshake, Connection, SendRequest};
use futures::{Async, Future, Poll}; use http::uri::Authority;
use h2::client::{handshake, Handshake};
use hashbrown::HashMap;
use http::uri::{Authority, Uri};
use indexmap::IndexSet; use indexmap::IndexSet;
use slab::Slab; use slab::Slab;
use tokio_timer::{sleep, Delay};
use super::connection::{ConnectionType, IoConnection}; use super::connection::{ConnectionType, IoConnection};
use super::error::ConnectError; use super::error::ConnectError;
use super::Connect;
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
/// Protocol version
pub enum Protocol { pub enum Protocol {
Http1, Http1,
Http2, Http2,
@ -39,15 +41,13 @@ impl From<Authority> for Key {
} }
/// Connections pool /// Connections pool
pub(crate) struct ConnectionPool<T, Io: AsyncRead + AsyncWrite + 'static>( pub(crate) struct ConnectionPool<T, Io: 'static>(Rc<RefCell<T>>, Rc<RefCell<Inner<Io>>>);
T,
Rc<RefCell<Inner<Io>>>,
);
impl<T, Io> ConnectionPool<T, Io> impl<T, Io> ConnectionPool<T, Io>
where where
Io: AsyncRead + AsyncWrite + 'static, Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Uri, Response = (Io, Protocol), Error = ConnectError>, T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{ {
pub(crate) fn new( pub(crate) fn new(
connector: T, connector: T,
@ -57,7 +57,7 @@ where
limit: usize, limit: usize,
) -> Self { ) -> Self {
ConnectionPool( ConnectionPool(
connector, Rc::new(RefCell::new(connector)),
Rc::new(RefCell::new(Inner { Rc::new(RefCell::new(Inner {
conn_lifetime, conn_lifetime,
conn_keep_alive, conn_keep_alive,
@ -66,8 +66,8 @@ where
acquired: 0, acquired: 0,
waiters: Slab::new(), waiters: Slab::new(),
waiters_queue: IndexSet::new(), waiters_queue: IndexSet::new(),
available: HashMap::new(), available: FxHashMap::default(),
task: AtomicTask::new(), waker: LocalWaker::new(),
})), })),
) )
} }
@ -75,8 +75,7 @@ where
impl<T, Io> Clone for ConnectionPool<T, Io> impl<T, Io> Clone for ConnectionPool<T, Io>
where where
T: Clone, Io: 'static,
Io: AsyncRead + AsyncWrite + 'static,
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
ConnectionPool(self.0.clone(), self.1.clone()) ConnectionPool(self.0.clone(), self.1.clone())
@ -85,74 +84,116 @@ where
impl<T, Io> Service for ConnectionPool<T, Io> impl<T, Io> Service for ConnectionPool<T, Io>
where where
Io: AsyncRead + AsyncWrite + 'static, Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Uri, Response = (Io, Protocol), Error = ConnectError>, T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{ {
type Request = Uri; type Request = Connect;
type Response = IoConnection<Io>; type Response = IoConnection<Io>;
type Error = ConnectError; type Error = ConnectError;
type Future = Either< type Future = LocalBoxFuture<'static, Result<IoConnection<Io>, ConnectError>>;
FutureResult<Self::Response, Self::Error>,
Either<WaitForConnection<Io>, OpenConnection<T::Future, Io>>,
>;
fn poll_ready(&mut self) -> Poll<(), Self::Error> { fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.0.poll_ready() self.0.poll_ready(cx)
} }
fn call(&mut self, req: Uri) -> Self::Future { fn call(&mut self, req: Connect) -> Self::Future {
let key = if let Some(authority) = req.authority_part() { // start support future
authority.clone().into() actix_rt::spawn(ConnectorPoolSupport {
} else { connector: self.0.clone(),
return Either::A(err(ConnectError::Unresolverd)); inner: self.1.clone(),
});
let mut connector = self.0.clone();
let inner = self.1.clone();
let fut = async move {
let key = if let Some(authority) = req.uri.authority() {
authority.clone().into()
} else {
return Err(ConnectError::Unresolverd);
};
// acquire connection
match poll_fn(|cx| Poll::Ready(inner.borrow_mut().acquire(&key, cx))).await {
Acquire::Acquired(io, created) => {
// use existing connection
return Ok(IoConnection::new(
io,
created,
Some(Acquired(key, Some(inner))),
));
}
Acquire::Available => {
// open tcp connection
let (io, proto) = connector.call(req).await?;
let guard = OpenGuard::new(key, inner);
if proto == Protocol::Http1 {
Ok(IoConnection::new(
ConnectionType::H1(io),
Instant::now(),
Some(guard.consume()),
))
} else {
let (snd, connection) = handshake(io).await?;
actix_rt::spawn(connection.map(|_| ()));
Ok(IoConnection::new(
ConnectionType::H2(snd),
Instant::now(),
Some(guard.consume()),
))
}
}
_ => {
// connection is not available, wait
let (rx, token) = inner.borrow_mut().wait_for(req);
let guard = WaiterGuard::new(key, token, inner);
let res = match rx.await {
Err(_) => Err(ConnectError::Disconnected),
Ok(res) => res,
};
guard.consume();
res
}
}
}; };
// acquire connection fut.boxed_local()
match self.1.as_ref().borrow_mut().acquire(&key) {
Acquire::Acquired(io, created) => {
// use existing connection
Either::A(ok(IoConnection::new(
io,
created,
Some(Acquired(key, Some(self.1.clone()))),
)))
}
Acquire::NotAvailable => {
// connection is not available, wait
let (rx, token) = self.1.as_ref().borrow_mut().wait_for(req);
Either::B(Either::A(WaitForConnection {
rx,
key,
token,
inner: Some(self.1.clone()),
}))
}
Acquire::Available => {
// open new connection
Either::B(Either::B(OpenConnection::new(
key,
self.1.clone(),
self.0.call(req),
)))
}
}
} }
} }
#[doc(hidden)] struct WaiterGuard<Io>
pub struct WaitForConnection<Io>
where where
Io: AsyncRead + AsyncWrite + 'static, Io: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
key: Key, key: Key,
token: usize, token: usize,
rx: oneshot::Receiver<Result<IoConnection<Io>, ConnectError>>,
inner: Option<Rc<RefCell<Inner<Io>>>>, inner: Option<Rc<RefCell<Inner<Io>>>>,
} }
impl<Io> Drop for WaitForConnection<Io> impl<Io> WaiterGuard<Io>
where where
Io: AsyncRead + AsyncWrite + 'static, Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
fn new(key: Key, token: usize, inner: Rc<RefCell<Inner<Io>>>) -> Self {
Self {
key,
token,
inner: Some(inner),
}
}
fn consume(mut self) {
let _ = self.inner.take();
}
}
impl<Io> Drop for WaiterGuard<Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(i) = self.inner.take() { if let Some(i) = self.inner.take() {
@ -163,121 +204,49 @@ where
} }
} }
impl<Io> Future for WaitForConnection<Io> struct OpenGuard<Io>
where where
Io: AsyncRead + AsyncWrite, Io: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
type Item = IoConnection<Io>;
type Error = ConnectError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
match self.rx.poll() {
Ok(Async::Ready(item)) => match item {
Err(err) => Err(err),
Ok(conn) => {
let _ = self.inner.take();
Ok(Async::Ready(conn))
}
},
Ok(Async::NotReady) => Ok(Async::NotReady),
Err(_) => {
let _ = self.inner.take();
Err(ConnectError::Disconnected)
}
}
}
}
#[doc(hidden)]
pub struct OpenConnection<F, Io>
where
Io: AsyncRead + AsyncWrite + 'static,
{
fut: F,
key: Key, key: Key,
h2: Option<Handshake<Io, Bytes>>,
inner: Option<Rc<RefCell<Inner<Io>>>>, inner: Option<Rc<RefCell<Inner<Io>>>>,
} }
impl<F, Io> OpenConnection<F, Io> impl<Io> OpenGuard<Io>
where where
F: Future<Item = (Io, Protocol), Error = ConnectError>, Io: AsyncRead + AsyncWrite + Unpin + 'static,
Io: AsyncRead + AsyncWrite + 'static,
{ {
fn new(key: Key, inner: Rc<RefCell<Inner<Io>>>, fut: F) -> Self { fn new(key: Key, inner: Rc<RefCell<Inner<Io>>>) -> Self {
OpenConnection { Self {
key, key,
fut,
inner: Some(inner), inner: Some(inner),
h2: None,
} }
} }
fn consume(mut self) -> Acquired<Io> {
Acquired(self.key.clone(), self.inner.take())
}
} }
impl<F, Io> Drop for OpenConnection<F, Io> impl<Io> Drop for OpenGuard<Io>
where where
Io: AsyncRead + AsyncWrite + 'static, Io: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(inner) = self.inner.take() { if let Some(i) = self.inner.take() {
let mut inner = inner.as_ref().borrow_mut(); let mut inner = i.as_ref().borrow_mut();
inner.release(); inner.release();
inner.check_availibility(); inner.check_availibility();
} }
} }
} }
impl<F, Io> Future for OpenConnection<F, Io>
where
F: Future<Item = (Io, Protocol), Error = ConnectError>,
Io: AsyncRead + AsyncWrite,
{
type Item = IoConnection<Io>;
type Error = ConnectError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
if let Some(ref mut h2) = self.h2 {
return match h2.poll() {
Ok(Async::Ready((snd, connection))) => {
tokio_current_thread::spawn(connection.map_err(|_| ()));
Ok(Async::Ready(IoConnection::new(
ConnectionType::H2(snd),
Instant::now(),
Some(Acquired(self.key.clone(), self.inner.clone())),
)))
}
Ok(Async::NotReady) => Ok(Async::NotReady),
Err(e) => Err(e.into()),
};
}
match self.fut.poll() {
Err(err) => Err(err),
Ok(Async::Ready((io, proto))) => {
let _ = self.inner.take();
if proto == Protocol::Http1 {
Ok(Async::Ready(IoConnection::new(
ConnectionType::H1(io),
Instant::now(),
Some(Acquired(self.key.clone(), self.inner.clone())),
)))
} else {
self.h2 = Some(handshake(io));
self.poll()
}
}
Ok(Async::NotReady) => Ok(Async::NotReady),
}
}
}
enum Acquire<T> { enum Acquire<T> {
Acquired(ConnectionType<T>, Instant), Acquired(ConnectionType<T>, Instant),
Available, Available,
NotAvailable, NotAvailable,
} }
// #[derive(Debug)]
struct AvailableConnection<Io> { struct AvailableConnection<Io> {
io: ConnectionType<Io>, io: ConnectionType<Io>,
used: Instant, used: Instant,
@ -290,10 +259,15 @@ pub(crate) struct Inner<Io> {
disconnect_timeout: Option<Duration>, disconnect_timeout: Option<Duration>,
limit: usize, limit: usize,
acquired: usize, acquired: usize,
available: HashMap<Key, VecDeque<AvailableConnection<Io>>>, available: FxHashMap<Key, VecDeque<AvailableConnection<Io>>>,
waiters: Slab<(Uri, oneshot::Sender<Result<IoConnection<Io>, ConnectError>>)>, waiters: Slab<
Option<(
Connect,
oneshot::Sender<Result<IoConnection<Io>, ConnectError>>,
)>,
>,
waiters_queue: IndexSet<(Key, usize)>, waiters_queue: IndexSet<(Key, usize)>,
task: AtomicTask, waker: LocalWaker,
} }
impl<Io> Inner<Io> { impl<Io> Inner<Io> {
@ -307,45 +281,34 @@ impl<Io> Inner<Io> {
fn release_waiter(&mut self, key: &Key, token: usize) { fn release_waiter(&mut self, key: &Key, token: usize) {
self.waiters.remove(token); self.waiters.remove(token);
self.waiters_queue.remove(&(key.clone(), token)); let _ = self.waiters_queue.shift_remove(&(key.clone(), token));
}
fn release_conn(&mut self, key: &Key, io: ConnectionType<Io>, created: Instant) {
self.acquired -= 1;
self.available
.entry(key.clone())
.or_insert_with(VecDeque::new)
.push_back(AvailableConnection {
io,
created,
used: Instant::now(),
});
} }
} }
impl<Io> Inner<Io> impl<Io> Inner<Io>
where where
Io: AsyncRead + AsyncWrite + 'static, Io: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
/// connection is not available, wait /// connection is not available, wait
fn wait_for( fn wait_for(
&mut self, &mut self,
connect: Uri, connect: Connect,
) -> ( ) -> (
oneshot::Receiver<Result<IoConnection<Io>, ConnectError>>, oneshot::Receiver<Result<IoConnection<Io>, ConnectError>>,
usize, usize,
) { ) {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let key: Key = connect.authority_part().unwrap().clone().into(); let key: Key = connect.uri.authority().unwrap().clone().into();
let entry = self.waiters.vacant_entry(); let entry = self.waiters.vacant_entry();
let token = entry.key(); let token = entry.key();
entry.insert((connect, tx)); entry.insert(Some((connect, tx)));
assert!(!self.waiters_queue.insert((key, token))); assert!(self.waiters_queue.insert((key, token)));
(rx, token) (rx, token)
} }
fn acquire(&mut self, key: &Key) -> Acquire<Io> { fn acquire(&mut self, key: &Key, cx: &mut Context<'_>) -> Acquire<Io> {
// check limits // check limits
if self.limit > 0 && self.acquired >= self.limit { if self.limit > 0 && self.acquired >= self.limit {
return Acquire::NotAvailable; return Acquire::NotAvailable;
@ -364,28 +327,26 @@ where
{ {
if let Some(timeout) = self.disconnect_timeout { if let Some(timeout) = self.disconnect_timeout {
if let ConnectionType::H1(io) = conn.io { if let ConnectionType::H1(io) = conn.io {
tokio_current_thread::spawn(CloseConnection::new( actix_rt::spawn(CloseConnection::new(io, timeout))
io, timeout,
))
} }
} }
} else { } else {
let mut io = conn.io; let mut io = conn.io;
let mut buf = [0; 2]; let mut buf = [0; 2];
if let ConnectionType::H1(ref mut s) = io { if let ConnectionType::H1(ref mut s) = io {
match s.read(&mut buf) { match Pin::new(s).poll_read(cx, &mut buf) {
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => (), Poll::Pending => (),
Ok(n) if n > 0 => { Poll::Ready(Ok(n)) if n > 0 => {
if let Some(timeout) = self.disconnect_timeout { if let Some(timeout) = self.disconnect_timeout {
if let ConnectionType::H1(io) = io { if let ConnectionType::H1(io) = io {
tokio_current_thread::spawn( actix_rt::spawn(CloseConnection::new(
CloseConnection::new(io, timeout), io, timeout,
) ))
} }
} }
continue; continue;
} }
Ok(_) | Err(_) => continue, _ => continue,
} }
} }
return Acquire::Acquired(io, conn.created); return Acquire::Acquired(io, conn.created);
@ -395,18 +356,32 @@ where
Acquire::Available Acquire::Available
} }
fn release_conn(&mut self, key: &Key, io: ConnectionType<Io>, created: Instant) {
self.acquired -= 1;
self.available
.entry(key.clone())
.or_insert_with(VecDeque::new)
.push_back(AvailableConnection {
io,
created,
used: Instant::now(),
});
self.check_availibility();
}
fn release_close(&mut self, io: ConnectionType<Io>) { fn release_close(&mut self, io: ConnectionType<Io>) {
self.acquired -= 1; self.acquired -= 1;
if let Some(timeout) = self.disconnect_timeout { if let Some(timeout) = self.disconnect_timeout {
if let ConnectionType::H1(io) = io { if let ConnectionType::H1(io) = io {
tokio_current_thread::spawn(CloseConnection::new(io, timeout)) actix_rt::spawn(CloseConnection::new(io, timeout))
} }
} }
self.check_availibility();
} }
fn check_availibility(&self) { fn check_availibility(&self) {
if !self.waiters_queue.is_empty() && self.acquired < self.limit { if !self.waiters_queue.is_empty() && self.acquired < self.limit {
self.task.notify() self.waker.wake();
} }
} }
} }
@ -418,39 +393,216 @@ struct CloseConnection<T> {
impl<T> CloseConnection<T> impl<T> CloseConnection<T>
where where
T: AsyncWrite, T: AsyncWrite + Unpin,
{ {
fn new(io: T, timeout: Duration) -> Self { fn new(io: T, timeout: Duration) -> Self {
CloseConnection { CloseConnection {
io, io,
timeout: sleep(timeout), timeout: delay_for(timeout),
} }
} }
} }
impl<T> Future for CloseConnection<T> impl<T> Future for CloseConnection<T>
where where
T: AsyncWrite, T: AsyncWrite + Unpin,
{ {
type Item = (); type Output = ();
type Error = ();
fn poll(&mut self) -> Poll<(), ()> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
match self.timeout.poll() { let this = self.get_mut();
Ok(Async::Ready(_)) | Err(_) => Ok(Async::Ready(())),
Ok(Async::NotReady) => match self.io.shutdown() { match Pin::new(&mut this.timeout).poll(cx) {
Ok(Async::Ready(_)) | Err(_) => Ok(Async::Ready(())), Poll::Ready(_) => Poll::Ready(()),
Ok(Async::NotReady) => Ok(Async::NotReady), Poll::Pending => match Pin::new(&mut this.io).poll_shutdown(cx) {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => Poll::Pending,
}, },
} }
} }
} }
struct ConnectorPoolSupport<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
connector: T,
inner: Rc<RefCell<Inner<Io>>>,
}
impl<T, Io> Future for ConnectorPoolSupport<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>,
T::Future: 'static,
{
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = unsafe { self.get_unchecked_mut() };
let mut inner = this.inner.as_ref().borrow_mut();
inner.waker.register(cx.waker());
// check waiters
loop {
let (key, token) = {
if let Some((key, token)) = inner.waiters_queue.get_index(0) {
(key.clone(), *token)
} else {
break;
}
};
if inner.waiters.get(token).unwrap().is_none() {
continue;
}
match inner.acquire(&key, cx) {
Acquire::NotAvailable => break,
Acquire::Acquired(io, created) => {
let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1;
if let Err(conn) = tx.send(Ok(IoConnection::new(
io,
created,
Some(Acquired(key.clone(), Some(this.inner.clone()))),
))) {
let (io, created) = conn.unwrap().into_inner();
inner.release_conn(&key, io, created);
}
}
Acquire::Available => {
let (connect, tx) =
inner.waiters.get_mut(token).unwrap().take().unwrap();
OpenWaitingConnection::spawn(
key.clone(),
tx,
this.inner.clone(),
this.connector.call(connect),
);
}
}
let _ = inner.waiters_queue.swap_remove_index(0);
}
Poll::Pending
}
}
struct OpenWaitingConnection<F, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
fut: F,
key: Key,
h2: Option<
LocalBoxFuture<
'static,
Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>,
>,
>,
rx: Option<oneshot::Sender<Result<IoConnection<Io>, ConnectError>>>,
inner: Option<Rc<RefCell<Inner<Io>>>>,
}
impl<F, Io> OpenWaitingConnection<F, Io>
where
F: Future<Output = Result<(Io, Protocol), ConnectError>> + 'static,
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
fn spawn(
key: Key,
rx: oneshot::Sender<Result<IoConnection<Io>, ConnectError>>,
inner: Rc<RefCell<Inner<Io>>>,
fut: F,
) {
actix_rt::spawn(OpenWaitingConnection {
key,
fut,
h2: None,
rx: Some(rx),
inner: Some(inner),
})
}
}
impl<F, Io> Drop for OpenWaitingConnection<F, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
fn drop(&mut self) {
if let Some(inner) = self.inner.take() {
let mut inner = inner.as_ref().borrow_mut();
inner.release();
inner.check_availibility();
}
}
}
impl<F, Io> Future for OpenWaitingConnection<F, Io>
where
F: Future<Output = Result<(Io, Protocol), ConnectError>>,
Io: AsyncRead + AsyncWrite + Unpin,
{
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = unsafe { self.get_unchecked_mut() };
if let Some(ref mut h2) = this.h2 {
return match Pin::new(h2).poll(cx) {
Poll::Ready(Ok((snd, connection))) => {
actix_rt::spawn(connection.map(|_| ()));
let rx = this.rx.take().unwrap();
let _ = rx.send(Ok(IoConnection::new(
ConnectionType::H2(snd),
Instant::now(),
Some(Acquired(this.key.clone(), this.inner.take())),
)));
Poll::Ready(())
}
Poll::Pending => Poll::Pending,
Poll::Ready(Err(err)) => {
let _ = this.inner.take();
if let Some(rx) = this.rx.take() {
let _ = rx.send(Err(ConnectError::H2(err)));
}
Poll::Ready(())
}
};
}
match unsafe { Pin::new_unchecked(&mut this.fut) }.poll(cx) {
Poll::Ready(Err(err)) => {
let _ = this.inner.take();
if let Some(rx) = this.rx.take() {
let _ = rx.send(Err(err));
}
Poll::Ready(())
}
Poll::Ready(Ok((io, proto))) => {
if proto == Protocol::Http1 {
let rx = this.rx.take().unwrap();
let _ = rx.send(Ok(IoConnection::new(
ConnectionType::H1(io),
Instant::now(),
Some(Acquired(this.key.clone(), this.inner.take())),
)));
Poll::Ready(())
} else {
this.h2 = Some(handshake(io).boxed_local());
unsafe { Pin::new_unchecked(this) }.poll(cx)
}
}
Poll::Pending => Poll::Pending,
}
}
}
pub(crate) struct Acquired<T>(Key, Option<Rc<RefCell<Inner<T>>>>); pub(crate) struct Acquired<T>(Key, Option<Rc<RefCell<Inner<T>>>>);
impl<T> Acquired<T> impl<T> Acquired<T>
where where
T: AsyncRead + AsyncWrite + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
pub(crate) fn close(&mut self, conn: IoConnection<T>) { pub(crate) fn close(&mut self, conn: IoConnection<T>) {
if let Some(inner) = self.1.take() { if let Some(inner) = self.1.take() {

View File

@ -0,0 +1,42 @@
use std::cell::UnsafeCell;
use std::rc::Rc;
use std::task::{Context, Poll};
use actix_service::Service;
#[doc(hidden)]
/// Service that allows to turn non-clone service to a service with `Clone` impl
pub(crate) struct CloneableService<T>(Rc<UnsafeCell<T>>);
impl<T> CloneableService<T> {
pub(crate) fn new(service: T) -> Self
where
T: Service,
{
Self(Rc::new(UnsafeCell::new(service)))
}
}
impl<T> Clone for CloneableService<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Service for CloneableService<T>
where
T: Service,
{
type Request = T::Request;
type Response = T::Response;
type Error = T::Error;
type Future = T::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
unsafe { &mut *self.0.as_ref().get() }.poll_ready(cx)
}
fn call(&mut self, req: T::Request) -> Self::Future {
unsafe { &mut *self.0.as_ref().get() }.call(req)
}
}

View File

@ -1,13 +1,13 @@
use std::cell::UnsafeCell; use std::cell::UnsafeCell;
use std::fmt;
use std::fmt::Write; use std::fmt::Write;
use std::rc::Rc; use std::rc::Rc;
use std::time::{Duration, Instant}; use std::time::Duration;
use std::{fmt, net};
use actix_rt::time::{delay_for, delay_until, Delay, Instant};
use bytes::BytesMut; use bytes::BytesMut;
use futures::{future, Future}; use futures_util::{future, FutureExt};
use time; use time;
use tokio_timer::{sleep, Delay};
// "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;
@ -47,6 +47,8 @@ struct Inner {
client_timeout: u64, client_timeout: u64,
client_disconnect: u64, client_disconnect: u64,
ka_enabled: bool, ka_enabled: bool,
secure: bool,
local_addr: Option<std::net::SocketAddr>,
timer: DateService, timer: DateService,
} }
@ -58,7 +60,7 @@ impl Clone for ServiceConfig {
impl Default for ServiceConfig { impl Default for ServiceConfig {
fn default() -> Self { fn default() -> Self {
Self::new(KeepAlive::Timeout(5), 0, 0) Self::new(KeepAlive::Timeout(5), 0, 0, false, None)
} }
} }
@ -68,6 +70,8 @@ impl ServiceConfig {
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_timeout: u64, client_timeout: u64,
client_disconnect: u64, client_disconnect: u64,
secure: bool,
local_addr: Option<net::SocketAddr>,
) -> ServiceConfig { ) -> ServiceConfig {
let (keep_alive, ka_enabled) = match keep_alive { let (keep_alive, ka_enabled) = match keep_alive {
KeepAlive::Timeout(val) => (val as u64, true), KeepAlive::Timeout(val) => (val as u64, true),
@ -85,10 +89,24 @@ impl ServiceConfig {
ka_enabled, ka_enabled,
client_timeout, client_timeout,
client_disconnect, client_disconnect,
secure,
local_addr,
timer: DateService::new(), timer: DateService::new(),
})) }))
} }
#[inline]
/// Returns true if connection is secure(https)
pub fn secure(&self) -> bool {
self.0.secure
}
#[inline]
/// Returns the local address that this server is bound to.
pub fn local_addr(&self) -> Option<net::SocketAddr> {
self.0.local_addr
}
#[inline] #[inline]
/// Keep alive duration if configured. /// Keep alive duration if configured.
pub fn keep_alive(&self) -> Option<Duration> { pub fn keep_alive(&self) -> Option<Duration> {
@ -104,10 +122,10 @@ impl ServiceConfig {
#[inline] #[inline]
/// Client timeout for first request. /// Client timeout for first request.
pub fn client_timer(&self) -> Option<Delay> { pub fn client_timer(&self) -> Option<Delay> {
let delay = self.0.client_timeout; let delay_time = self.0.client_timeout;
if delay != 0 { if delay_time != 0 {
Some(Delay::new( Some(delay_until(
self.0.timer.now() + Duration::from_millis(delay), self.0.timer.now() + Duration::from_millis(delay_time),
)) ))
} else { } else {
None None
@ -138,7 +156,7 @@ impl ServiceConfig {
/// Return keep-alive timer delay is configured. /// Return keep-alive timer delay is configured.
pub fn keep_alive_timer(&self) -> Option<Delay> { pub fn keep_alive_timer(&self) -> Option<Delay> {
if let Some(ka) = self.0.keep_alive { if let Some(ka) = self.0.keep_alive {
Some(Delay::new(self.0.timer.now() + ka)) Some(delay_until(self.0.timer.now() + ka))
} else { } else {
None None
} }
@ -158,19 +176,25 @@ impl ServiceConfig {
self.0.timer.now() self.0.timer.now()
} }
pub(crate) fn set_date(&self, dst: &mut BytesMut) { #[doc(hidden)]
pub fn set_date(&self, dst: &mut BytesMut) {
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: ");
buf[6..35].copy_from_slice(&self.0.timer.date().bytes); self.0
.timer
.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);
} }
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) { pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
dst.extend_from_slice(&self.0.timer.date().bytes); self.0
.timer
.set_date(|date| dst.extend_from_slice(&date.bytes));
} }
} }
#[derive(Copy, Clone)]
struct Date { struct Date {
bytes: [u8; DATE_VALUE_LENGTH], bytes: [u8; DATE_VALUE_LENGTH],
pos: usize, pos: usize,
@ -214,10 +238,6 @@ impl DateServiceInner {
} }
} }
fn get_ref(&self) -> &Option<(Date, Instant)> {
unsafe { &*self.current.get() }
}
fn reset(&self) { fn reset(&self) {
unsafe { (&mut *self.current.get()).take() }; unsafe { (&mut *self.current.get()).take() };
} }
@ -235,56 +255,45 @@ impl DateService {
} }
fn check_date(&self) { fn check_date(&self) {
if self.0.get_ref().is_none() { if unsafe { (&*self.0.current.get()).is_none() } {
self.0.update(); self.0.update();
// periodic date update // periodic date update
let s = self.clone(); let s = self.clone();
tokio_current_thread::spawn(sleep(Duration::from_millis(500)).then( actix_rt::spawn(delay_for(Duration::from_millis(500)).then(move |_| {
move |_| { s.0.reset();
s.0.reset(); future::ready(())
future::ok(()) }));
},
));
} }
} }
fn now(&self) -> Instant { fn now(&self) -> Instant {
self.check_date(); self.check_date();
self.0.get_ref().as_ref().unwrap().1 unsafe { (&*self.0.current.get()).as_ref().unwrap().1 }
} }
fn date(&self) -> &Date { fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
self.check_date(); self.check_date();
f(&unsafe { (&*self.0.current.get()).as_ref().unwrap().0 })
let item = self.0.get_ref().as_ref().unwrap();
&item.0
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use actix_rt::System;
use futures::future;
#[test] #[test]
fn test_date_len() { fn test_date_len() {
assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len()); assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len());
} }
#[test] #[actix_rt::test]
fn test_date() { async fn test_date() {
let mut rt = System::new("test"); let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
let _ = rt.block_on(future::lazy(|| { settings.set_date(&mut buf1);
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0); let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); settings.set_date(&mut buf2);
settings.set_date(&mut buf1); assert_eq!(buf1, buf2);
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf2);
assert_eq!(buf1, buf2);
future::ok::<_, ()>(())
}));
} }
} }

View File

@ -0,0 +1,250 @@
use std::borrow::Cow;
use chrono::Duration;
use time::Tm;
use super::{Cookie, SameSite};
/// Structure that follows the builder pattern for building `Cookie` structs.
///
/// To construct a cookie:
///
/// 1. Call [`Cookie::build`](struct.Cookie.html#method.build) to start building.
/// 2. Use any of the builder methods to set fields in the cookie.
/// 3. Call [finish](#method.finish) to retrieve the built cookie.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let cookie: Cookie = Cookie::build("name", "value")
/// .domain("www.rust-lang.org")
/// .path("/")
/// .secure(true)
/// .http_only(true)
/// .max_age(84600)
/// .finish();
/// ```
#[derive(Debug, Clone)]
pub struct CookieBuilder {
/// The cookie being built.
cookie: Cookie<'static>,
}
impl CookieBuilder {
/// Creates a new `CookieBuilder` instance from the given name and value.
///
/// This method is typically called indirectly via
/// [Cookie::build](struct.Cookie.html#method.build).
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let c = Cookie::build("foo", "bar").finish();
/// assert_eq!(c.name_value(), ("foo", "bar"));
/// ```
pub fn new<N, V>(name: N, value: V) -> CookieBuilder
where
N: Into<Cow<'static, str>>,
V: Into<Cow<'static, str>>,
{
CookieBuilder {
cookie: Cookie::new(name, value),
}
}
/// Sets the `expires` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let c = Cookie::build("foo", "bar")
/// .expires(time::now())
/// .finish();
///
/// assert!(c.expires().is_some());
/// ```
#[inline]
pub fn expires(mut self, when: Tm) -> CookieBuilder {
self.cookie.set_expires(when);
self
}
/// Sets the `max_age` field in seconds in the cookie being built.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let c = Cookie::build("foo", "bar")
/// .max_age(1800)
/// .finish();
///
/// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60)));
/// ```
#[inline]
pub fn max_age(self, seconds: i64) -> CookieBuilder {
self.max_age_time(Duration::seconds(seconds))
}
/// Sets the `max_age` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let c = Cookie::build("foo", "bar")
/// .max_age_time(time::Duration::minutes(30))
/// .finish();
///
/// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60)));
/// ```
#[inline]
pub fn max_age_time(mut self, value: Duration) -> CookieBuilder {
self.cookie.set_max_age(value);
self
}
/// Sets the `domain` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let c = Cookie::build("foo", "bar")
/// .domain("www.rust-lang.org")
/// .finish();
///
/// assert_eq!(c.domain(), Some("www.rust-lang.org"));
/// ```
pub fn domain<D: Into<Cow<'static, str>>>(mut self, value: D) -> CookieBuilder {
self.cookie.set_domain(value);
self
}
/// Sets the `path` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let c = Cookie::build("foo", "bar")
/// .path("/")
/// .finish();
///
/// assert_eq!(c.path(), Some("/"));
/// ```
pub fn path<P: Into<Cow<'static, str>>>(mut self, path: P) -> CookieBuilder {
self.cookie.set_path(path);
self
}
/// Sets the `secure` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let c = Cookie::build("foo", "bar")
/// .secure(true)
/// .finish();
///
/// assert_eq!(c.secure(), Some(true));
/// ```
#[inline]
pub fn secure(mut self, value: bool) -> CookieBuilder {
self.cookie.set_secure(value);
self
}
/// Sets the `http_only` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let c = Cookie::build("foo", "bar")
/// .http_only(true)
/// .finish();
///
/// assert_eq!(c.http_only(), Some(true));
/// ```
#[inline]
pub fn http_only(mut self, value: bool) -> CookieBuilder {
self.cookie.set_http_only(value);
self
}
/// Sets the `same_site` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{Cookie, SameSite};
///
/// let c = Cookie::build("foo", "bar")
/// .same_site(SameSite::Strict)
/// .finish();
///
/// assert_eq!(c.same_site(), Some(SameSite::Strict));
/// ```
#[inline]
pub fn same_site(mut self, value: SameSite) -> CookieBuilder {
self.cookie.set_same_site(value);
self
}
/// Makes the cookie being built 'permanent' by extending its expiration and
/// max age 20 years into the future.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
/// use chrono::Duration;
///
/// let c = Cookie::build("foo", "bar")
/// .permanent()
/// .finish();
///
/// assert_eq!(c.max_age(), Some(Duration::days(365 * 20)));
/// # assert!(c.expires().is_some());
/// ```
#[inline]
pub fn permanent(mut self) -> CookieBuilder {
self.cookie.make_permanent();
self
}
/// Finishes building and returns the built `Cookie`.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Cookie;
///
/// let c = Cookie::build("foo", "bar")
/// .domain("crates.io")
/// .path("/")
/// .finish();
///
/// assert_eq!(c.name_value(), ("foo", "bar"));
/// assert_eq!(c.domain(), Some("crates.io"));
/// assert_eq!(c.path(), Some("/"));
/// ```
#[inline]
pub fn finish(self) -> Cookie<'static> {
self.cookie
}
}

View File

@ -0,0 +1,71 @@
use std::borrow::Borrow;
use std::hash::{Hash, Hasher};
use std::ops::{Deref, DerefMut};
use super::Cookie;
/// A `DeltaCookie` is a helper structure used in a cookie jar. It wraps a
/// `Cookie` so that it can be hashed and compared purely by name. It further
/// records whether the wrapped cookie is a "removal" cookie, that is, a cookie
/// that when sent to the client removes the named cookie on the client's
/// machine.
#[derive(Clone, Debug)]
pub struct DeltaCookie {
pub cookie: Cookie<'static>,
pub removed: bool,
}
impl DeltaCookie {
/// Create a new `DeltaCookie` that is being added to a jar.
#[inline]
pub fn added(cookie: Cookie<'static>) -> DeltaCookie {
DeltaCookie {
cookie,
removed: false,
}
}
/// Create a new `DeltaCookie` that is being removed from a jar. The
/// `cookie` should be a "removal" cookie.
#[inline]
pub fn removed(cookie: Cookie<'static>) -> DeltaCookie {
DeltaCookie {
cookie,
removed: true,
}
}
}
impl Deref for DeltaCookie {
type Target = Cookie<'static>;
fn deref(&self) -> &Cookie<'static> {
&self.cookie
}
}
impl DerefMut for DeltaCookie {
fn deref_mut(&mut self) -> &mut Cookie<'static> {
&mut self.cookie
}
}
impl PartialEq for DeltaCookie {
fn eq(&self, other: &DeltaCookie) -> bool {
self.name() == other.name()
}
}
impl Eq for DeltaCookie {}
impl Hash for DeltaCookie {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name().hash(state);
}
}
impl Borrow<str> for DeltaCookie {
fn borrow(&self) -> &str {
self.name()
}
}

View File

@ -0,0 +1,98 @@
//! This module contains types that represent cookie properties that are not yet
//! standardized. That is, _draft_ features.
use std::fmt;
/// The `SameSite` cookie attribute.
///
/// A cookie with a `SameSite` attribute is imposed restrictions on when it is
/// sent to the origin server in a cross-site request. If the `SameSite`
/// attribute is "Strict", then the cookie is never sent in cross-site requests.
/// If the `SameSite` attribute is "Lax", the cookie is only sent in cross-site
/// requests with "safe" HTTP methods, i.e, `GET`, `HEAD`, `OPTIONS`, `TRACE`.
/// If the `SameSite` attribute is not present (made explicit via the
/// `SameSite::None` variant), then the cookie will be sent as normal.
///
/// **Note:** This cookie attribute is an HTTP draft! Its meaning and definition
/// are subject to change.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SameSite {
/// The "Strict" `SameSite` attribute.
Strict,
/// The "Lax" `SameSite` attribute.
Lax,
/// No `SameSite` attribute.
None,
}
impl SameSite {
/// Returns `true` if `self` is `SameSite::Strict` and `false` otherwise.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::SameSite;
///
/// let strict = SameSite::Strict;
/// assert!(strict.is_strict());
/// assert!(!strict.is_lax());
/// assert!(!strict.is_none());
/// ```
#[inline]
pub fn is_strict(self) -> bool {
match self {
SameSite::Strict => true,
SameSite::Lax | SameSite::None => false,
}
}
/// Returns `true` if `self` is `SameSite::Lax` and `false` otherwise.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::SameSite;
///
/// let lax = SameSite::Lax;
/// assert!(lax.is_lax());
/// assert!(!lax.is_strict());
/// assert!(!lax.is_none());
/// ```
#[inline]
pub fn is_lax(self) -> bool {
match self {
SameSite::Lax => true,
SameSite::Strict | SameSite::None => false,
}
}
/// Returns `true` if `self` is `SameSite::None` and `false` otherwise.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::SameSite;
///
/// let none = SameSite::None;
/// assert!(none.is_none());
/// assert!(!none.is_lax());
/// assert!(!none.is_strict());
/// ```
#[inline]
pub fn is_none(self) -> bool {
match self {
SameSite::None => true,
SameSite::Lax | SameSite::Strict => false,
}
}
}
impl fmt::Display for SameSite {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
SameSite::Strict => write!(f, "Strict"),
SameSite::Lax => write!(f, "Lax"),
SameSite::None => Ok(()),
}
}
}

View File

@ -0,0 +1,651 @@
use std::collections::HashSet;
use std::mem::replace;
use chrono::Duration;
use super::delta::DeltaCookie;
use super::Cookie;
#[cfg(feature = "secure-cookies")]
use super::secure::{Key, PrivateJar, SignedJar};
/// A collection of cookies that tracks its modifications.
///
/// A `CookieJar` provides storage for any number of cookies. Any changes made
/// to the jar are tracked; the changes can be retrieved via the
/// [delta](#method.delta) method which returns an interator over the changes.
///
/// # Usage
///
/// A jar's life begins via [new](#method.new) and calls to
/// [`add_original`](#method.add_original):
///
/// ```rust
/// use actix_http::cookie::{Cookie, CookieJar};
///
/// let mut jar = CookieJar::new();
/// jar.add_original(Cookie::new("name", "value"));
/// jar.add_original(Cookie::new("second", "another"));
/// ```
///
/// Cookies can be added via [add](#method.add) and removed via
/// [remove](#method.remove). Finally, cookies can be looked up via
/// [get](#method.get):
///
/// ```rust
/// # use actix_http::cookie::{Cookie, CookieJar};
/// let mut jar = CookieJar::new();
/// jar.add(Cookie::new("a", "one"));
/// jar.add(Cookie::new("b", "two"));
///
/// assert_eq!(jar.get("a").map(|c| c.value()), Some("one"));
/// assert_eq!(jar.get("b").map(|c| c.value()), Some("two"));
///
/// jar.remove(Cookie::named("b"));
/// assert!(jar.get("b").is_none());
/// ```
///
/// # Deltas
///
/// A jar keeps track of any modifications made to it over time. The
/// modifications are recorded as cookies. The modifications can be retrieved
/// via [delta](#method.delta). Any new `Cookie` added to a jar via `add`
/// results in the same `Cookie` appearing in the `delta`; cookies added via
/// `add_original` do not count towards the delta. Any _original_ cookie that is
/// removed from a jar results in a "removal" cookie appearing in the delta. A
/// "removal" cookie is a cookie that a server sends so that the cookie is
/// removed from the client's machine.
///
/// Deltas are typically used to create `Set-Cookie` headers corresponding to
/// the changes made to a cookie jar over a period of time.
///
/// ```rust
/// # use actix_http::cookie::{Cookie, CookieJar};
/// let mut jar = CookieJar::new();
///
/// // original cookies don't affect the delta
/// jar.add_original(Cookie::new("original", "value"));
/// assert_eq!(jar.delta().count(), 0);
///
/// // new cookies result in an equivalent `Cookie` in the delta
/// jar.add(Cookie::new("a", "one"));
/// jar.add(Cookie::new("b", "two"));
/// assert_eq!(jar.delta().count(), 2);
///
/// // removing an original cookie adds a "removal" cookie to the delta
/// jar.remove(Cookie::named("original"));
/// assert_eq!(jar.delta().count(), 3);
///
/// // removing a new cookie that was added removes that `Cookie` from the delta
/// jar.remove(Cookie::named("a"));
/// assert_eq!(jar.delta().count(), 2);
/// ```
#[derive(Default, Debug, Clone)]
pub struct CookieJar {
original_cookies: HashSet<DeltaCookie>,
delta_cookies: HashSet<DeltaCookie>,
}
impl CookieJar {
/// Creates an empty cookie jar.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::CookieJar;
///
/// let jar = CookieJar::new();
/// assert_eq!(jar.iter().count(), 0);
/// ```
pub fn new() -> CookieJar {
CookieJar::default()
}
/// Returns a reference to the `Cookie` inside this jar with the name
/// `name`. If no such cookie exists, returns `None`.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
/// assert!(jar.get("name").is_none());
///
/// jar.add(Cookie::new("name", "value"));
/// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
/// ```
pub fn get(&self, name: &str) -> Option<&Cookie<'static>> {
self.delta_cookies
.get(name)
.or_else(|| self.original_cookies.get(name))
.and_then(|c| if !c.removed { Some(&c.cookie) } else { None })
}
/// Adds an "original" `cookie` to this jar. If an original cookie with the
/// same name already exists, it is replaced with `cookie`. Cookies added
/// with `add` take precedence and are not replaced by this method.
///
/// Adding an original cookie does not affect the [delta](#method.delta)
/// computation. This method is intended to be used to seed the cookie jar
/// with cookies received from a client's HTTP message.
///
/// For accurate `delta` computations, this method should not be called
/// after calling `remove`.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
/// jar.add_original(Cookie::new("name", "value"));
/// jar.add_original(Cookie::new("second", "two"));
///
/// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
/// assert_eq!(jar.get("second").map(|c| c.value()), Some("two"));
/// assert_eq!(jar.iter().count(), 2);
/// assert_eq!(jar.delta().count(), 0);
/// ```
pub fn add_original(&mut self, cookie: Cookie<'static>) {
self.original_cookies.replace(DeltaCookie::added(cookie));
}
/// Adds `cookie` to this jar. If a cookie with the same name already
/// exists, it is replaced with `cookie`.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
/// jar.add(Cookie::new("name", "value"));
/// jar.add(Cookie::new("second", "two"));
///
/// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
/// assert_eq!(jar.get("second").map(|c| c.value()), Some("two"));
/// assert_eq!(jar.iter().count(), 2);
/// assert_eq!(jar.delta().count(), 2);
/// ```
pub fn add(&mut self, cookie: Cookie<'static>) {
self.delta_cookies.replace(DeltaCookie::added(cookie));
}
/// Removes `cookie` from this jar. If an _original_ cookie with the same
/// name as `cookie` is present in the jar, a _removal_ cookie will be
/// present in the `delta` computation. To properly generate the removal
/// cookie, `cookie` must contain the same `path` and `domain` as the cookie
/// that was initially set.
///
/// A "removal" cookie is a cookie that has the same name as the original
/// cookie but has an empty value, a max-age of 0, and an expiration date
/// far in the past.
///
/// # Example
///
/// Removing an _original_ cookie results in a _removal_ cookie:
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie};
/// use chrono::Duration;
///
/// let mut jar = CookieJar::new();
///
/// // Assume this cookie originally had a path of "/" and domain of "a.b".
/// jar.add_original(Cookie::new("name", "value"));
///
/// // If the path and domain were set, they must be provided to `remove`.
/// jar.remove(Cookie::build("name", "").path("/").domain("a.b").finish());
///
/// // The delta will contain the removal cookie.
/// let delta: Vec<_> = jar.delta().collect();
/// assert_eq!(delta.len(), 1);
/// assert_eq!(delta[0].name(), "name");
/// assert_eq!(delta[0].max_age(), Some(Duration::seconds(0)));
/// ```
///
/// Removing a new cookie does not result in a _removal_ cookie:
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
/// jar.add(Cookie::new("name", "value"));
/// assert_eq!(jar.delta().count(), 1);
///
/// jar.remove(Cookie::named("name"));
/// assert_eq!(jar.delta().count(), 0);
/// ```
pub fn remove(&mut self, mut cookie: Cookie<'static>) {
if self.original_cookies.contains(cookie.name()) {
cookie.set_value("");
cookie.set_max_age(Duration::seconds(0));
cookie.set_expires(time::now() - Duration::days(365));
self.delta_cookies.replace(DeltaCookie::removed(cookie));
} else {
self.delta_cookies.remove(cookie.name());
}
}
/// Removes `cookie` from this jar completely. This method differs from
/// `remove` in that no delta cookie is created under any condition. Neither
/// the `delta` nor `iter` methods will return a cookie that is removed
/// using this method.
///
/// # Example
///
/// Removing an _original_ cookie; no _removal_ cookie is generated:
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie};
/// use chrono::Duration;
///
/// let mut jar = CookieJar::new();
///
/// // Add an original cookie and a new cookie.
/// jar.add_original(Cookie::new("name", "value"));
/// jar.add(Cookie::new("key", "value"));
/// assert_eq!(jar.delta().count(), 1);
/// assert_eq!(jar.iter().count(), 2);
///
/// // Now force remove the original cookie.
/// jar.force_remove(Cookie::new("name", "value"));
/// assert_eq!(jar.delta().count(), 1);
/// assert_eq!(jar.iter().count(), 1);
///
/// // Now force remove the new cookie.
/// jar.force_remove(Cookie::new("key", "value"));
/// assert_eq!(jar.delta().count(), 0);
/// assert_eq!(jar.iter().count(), 0);
/// ```
pub fn force_remove<'a>(&mut self, cookie: Cookie<'a>) {
self.original_cookies.remove(cookie.name());
self.delta_cookies.remove(cookie.name());
}
/// Removes all cookies from this cookie jar.
#[deprecated(
since = "0.7.0",
note = "calling this method may not remove \
all cookies since the path and domain are not specified; use \
`remove` instead"
)]
pub fn clear(&mut self) {
self.delta_cookies.clear();
for delta in replace(&mut self.original_cookies, HashSet::new()) {
self.remove(delta.cookie);
}
}
/// Returns an iterator over cookies that represent the changes to this jar
/// over time. These cookies can be rendered directly as `Set-Cookie` header
/// values to affect the changes made to this jar on the client.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
/// jar.add_original(Cookie::new("name", "value"));
/// jar.add_original(Cookie::new("second", "two"));
///
/// // Add new cookies.
/// jar.add(Cookie::new("new", "third"));
/// jar.add(Cookie::new("another", "fourth"));
/// jar.add(Cookie::new("yac", "fifth"));
///
/// // Remove some cookies.
/// jar.remove(Cookie::named("name"));
/// jar.remove(Cookie::named("another"));
///
/// // Delta contains two new cookies ("new", "yac") and a removal ("name").
/// assert_eq!(jar.delta().count(), 3);
/// ```
pub fn delta(&self) -> Delta<'_> {
Delta {
iter: self.delta_cookies.iter(),
}
}
/// Returns an iterator over all of the cookies present in this jar.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
///
/// jar.add_original(Cookie::new("name", "value"));
/// jar.add_original(Cookie::new("second", "two"));
///
/// jar.add(Cookie::new("new", "third"));
/// jar.add(Cookie::new("another", "fourth"));
/// jar.add(Cookie::new("yac", "fifth"));
///
/// jar.remove(Cookie::named("name"));
/// jar.remove(Cookie::named("another"));
///
/// // There are three cookies in the jar: "second", "new", and "yac".
/// # assert_eq!(jar.iter().count(), 3);
/// for cookie in jar.iter() {
/// match cookie.name() {
/// "second" => assert_eq!(cookie.value(), "two"),
/// "new" => assert_eq!(cookie.value(), "third"),
/// "yac" => assert_eq!(cookie.value(), "fifth"),
/// _ => unreachable!("there are only three cookies in the jar")
/// }
/// }
/// ```
pub fn iter(&self) -> Iter<'_> {
Iter {
delta_cookies: self
.delta_cookies
.iter()
.chain(self.original_cookies.difference(&self.delta_cookies)),
}
}
/// Returns a `PrivateJar` with `self` as its parent jar using the key `key`
/// to sign/encrypt and verify/decrypt cookies added/retrieved from the
/// child jar.
///
/// Any modifications to the child jar will be reflected on the parent jar,
/// and any retrievals from the child jar will be made from the parent jar.
///
/// This method is only available when the `secure` feature is enabled.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{Cookie, CookieJar, Key};
///
/// // Generate a secure key.
/// let key = Key::generate();
///
/// // Add a private (signed + encrypted) cookie.
/// let mut jar = CookieJar::new();
/// jar.private(&key).add(Cookie::new("private", "text"));
///
/// // The cookie's contents are encrypted.
/// assert_ne!(jar.get("private").unwrap().value(), "text");
///
/// // They can be decrypted and verified through the child jar.
/// assert_eq!(jar.private(&key).get("private").unwrap().value(), "text");
///
/// // A tampered with cookie does not validate but still exists.
/// let mut cookie = jar.get("private").unwrap().clone();
/// jar.add(Cookie::new("private", cookie.value().to_string() + "!"));
/// assert!(jar.private(&key).get("private").is_none());
/// assert!(jar.get("private").is_some());
/// ```
#[cfg(feature = "secure-cookies")]
pub fn private(&mut self, key: &Key) -> PrivateJar<'_> {
PrivateJar::new(self, key)
}
/// Returns a `SignedJar` with `self` as its parent jar using the key `key`
/// to sign/verify cookies added/retrieved from the child jar.
///
/// Any modifications to the child jar will be reflected on the parent jar,
/// and any retrievals from the child jar will be made from the parent jar.
///
/// This method is only available when the `secure` feature is enabled.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{Cookie, CookieJar, Key};
///
/// // Generate a secure key.
/// let key = Key::generate();
///
/// // Add a signed cookie.
/// let mut jar = CookieJar::new();
/// jar.signed(&key).add(Cookie::new("signed", "text"));
///
/// // The cookie's contents are signed but still in plaintext.
/// assert_ne!(jar.get("signed").unwrap().value(), "text");
/// assert!(jar.get("signed").unwrap().value().contains("text"));
///
/// // They can be verified through the child jar.
/// assert_eq!(jar.signed(&key).get("signed").unwrap().value(), "text");
///
/// // A tampered with cookie does not validate but still exists.
/// let mut cookie = jar.get("signed").unwrap().clone();
/// jar.add(Cookie::new("signed", cookie.value().to_string() + "!"));
/// assert!(jar.signed(&key).get("signed").is_none());
/// assert!(jar.get("signed").is_some());
/// ```
#[cfg(feature = "secure-cookies")]
pub fn signed(&mut self, key: &Key) -> SignedJar<'_> {
SignedJar::new(self, key)
}
}
use std::collections::hash_set::Iter as HashSetIter;
/// Iterator over the changes to a cookie jar.
pub struct Delta<'a> {
iter: HashSetIter<'a, DeltaCookie>,
}
impl<'a> Iterator for Delta<'a> {
type Item = &'a Cookie<'static>;
fn next(&mut self) -> Option<&'a Cookie<'static>> {
self.iter.next().map(|c| &c.cookie)
}
}
use std::collections::hash_map::RandomState;
use std::collections::hash_set::Difference;
use std::iter::Chain;
/// Iterator over all of the cookies in a jar.
pub struct Iter<'a> {
delta_cookies:
Chain<HashSetIter<'a, DeltaCookie>, Difference<'a, DeltaCookie, RandomState>>,
}
impl<'a> Iterator for Iter<'a> {
type Item = &'a Cookie<'static>;
fn next(&mut self) -> Option<&'a Cookie<'static>> {
for cookie in self.delta_cookies.by_ref() {
if !cookie.removed {
return Some(&*cookie);
}
}
None
}
}
#[cfg(test)]
mod test {
#[cfg(feature = "secure-cookies")]
use super::Key;
use super::{Cookie, CookieJar};
#[test]
#[allow(deprecated)]
fn simple() {
let mut c = CookieJar::new();
c.add(Cookie::new("test", ""));
c.add(Cookie::new("test2", ""));
c.remove(Cookie::named("test"));
assert!(c.get("test").is_none());
assert!(c.get("test2").is_some());
c.add(Cookie::new("test3", ""));
c.clear();
assert!(c.get("test").is_none());
assert!(c.get("test2").is_none());
assert!(c.get("test3").is_none());
}
#[test]
fn jar_is_send() {
fn is_send<T: Send>(_: T) -> bool {
true
}
assert!(is_send(CookieJar::new()))
}
#[test]
#[cfg(feature = "secure-cookies")]
fn iter() {
let key = Key::generate();
let mut c = CookieJar::new();
c.add_original(Cookie::new("original", "original"));
c.add(Cookie::new("test", "test"));
c.add(Cookie::new("test2", "test2"));
c.add(Cookie::new("test3", "test3"));
assert_eq!(c.iter().count(), 4);
c.signed(&key).add(Cookie::new("signed", "signed"));
c.private(&key).add(Cookie::new("encrypted", "encrypted"));
assert_eq!(c.iter().count(), 6);
c.remove(Cookie::named("test"));
assert_eq!(c.iter().count(), 5);
c.remove(Cookie::named("signed"));
c.remove(Cookie::named("test2"));
assert_eq!(c.iter().count(), 3);
c.add(Cookie::new("test2", "test2"));
assert_eq!(c.iter().count(), 4);
c.remove(Cookie::named("test2"));
assert_eq!(c.iter().count(), 3);
}
#[test]
#[cfg(feature = "secure-cookies")]
fn delta() {
use chrono::Duration;
use std::collections::HashMap;
let mut c = CookieJar::new();
c.add_original(Cookie::new("original", "original"));
c.add_original(Cookie::new("original1", "original1"));
c.add(Cookie::new("test", "test"));
c.add(Cookie::new("test2", "test2"));
c.add(Cookie::new("test3", "test3"));
c.add(Cookie::new("test4", "test4"));
c.remove(Cookie::named("test"));
c.remove(Cookie::named("original"));
assert_eq!(c.delta().count(), 4);
let names: HashMap<_, _> = c.delta().map(|c| (c.name(), c.max_age())).collect();
assert!(names.get("test2").unwrap().is_none());
assert!(names.get("test3").unwrap().is_none());
assert!(names.get("test4").unwrap().is_none());
assert_eq!(names.get("original").unwrap(), &Some(Duration::seconds(0)));
}
#[test]
fn replace_original() {
let mut jar = CookieJar::new();
jar.add_original(Cookie::new("original_a", "a"));
jar.add_original(Cookie::new("original_b", "b"));
assert_eq!(jar.get("original_a").unwrap().value(), "a");
jar.add(Cookie::new("original_a", "av2"));
assert_eq!(jar.get("original_a").unwrap().value(), "av2");
}
#[test]
fn empty_delta() {
let mut jar = CookieJar::new();
jar.add(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 1);
jar.remove(Cookie::named("name"));
assert_eq!(jar.delta().count(), 0);
jar.add_original(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 0);
jar.remove(Cookie::named("name"));
assert_eq!(jar.delta().count(), 1);
jar.add(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 1);
jar.remove(Cookie::named("name"));
assert_eq!(jar.delta().count(), 1);
}
#[test]
fn add_remove_add() {
let mut jar = CookieJar::new();
jar.add_original(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 0);
jar.remove(Cookie::named("name"));
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
// The cookie's been deleted. Another original doesn't change that.
jar.add_original(Cookie::new("name", "val"));
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
jar.remove(Cookie::named("name"));
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
jar.add(Cookie::new("name", "val"));
assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
jar.remove(Cookie::named("name"));
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
}
#[test]
fn replace_remove() {
let mut jar = CookieJar::new();
jar.add_original(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 0);
jar.add(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 1);
assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1);
jar.remove(Cookie::named("name"));
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
}
#[test]
fn remove_with_path() {
let mut jar = CookieJar::new();
jar.add_original(Cookie::build("name", "val").finish());
assert_eq!(jar.iter().count(), 1);
assert_eq!(jar.delta().count(), 0);
assert_eq!(jar.iter().filter(|c| c.path().is_none()).count(), 1);
jar.remove(Cookie::build("name", "").path("/").finish());
assert_eq!(jar.iter().count(), 0);
assert_eq!(jar.delta().count(), 1);
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().filter(|c| c.path() == Some("/")).count(), 1);
}
}

1100
actix-http/src/cookie/mod.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,421 @@
use std::borrow::Cow;
use std::cmp;
use std::convert::From;
use std::error::Error;
use std::fmt;
use std::str::Utf8Error;
use chrono::Duration;
use percent_encoding::percent_decode;
use super::{Cookie, CookieStr, SameSite};
/// Enum corresponding to a parsing error.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ParseError {
/// The cookie did not contain a name/value pair.
MissingPair,
/// The cookie's name was empty.
EmptyName,
/// Decoding the cookie's name or value resulted in invalid UTF-8.
Utf8Error(Utf8Error),
/// It is discouraged to exhaustively match on this enum as its variants may
/// grow without a breaking-change bump in version numbers.
#[doc(hidden)]
__Nonexhasutive,
}
impl ParseError {
/// Returns a description of this error as a string
pub fn as_str(&self) -> &'static str {
match *self {
ParseError::MissingPair => "the cookie is missing a name/value pair",
ParseError::EmptyName => "the cookie's name is empty",
ParseError::Utf8Error(_) => {
"decoding the cookie's name or value resulted in invalid UTF-8"
}
ParseError::__Nonexhasutive => unreachable!("__Nonexhasutive ParseError"),
}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<Utf8Error> for ParseError {
fn from(error: Utf8Error) -> ParseError {
ParseError::Utf8Error(error)
}
}
impl Error for ParseError {}
fn indexes_of(needle: &str, haystack: &str) -> Option<(usize, usize)> {
let haystack_start = haystack.as_ptr() as usize;
let needle_start = needle.as_ptr() as usize;
if needle_start < haystack_start {
return None;
}
if (needle_start + needle.len()) > (haystack_start + haystack.len()) {
return None;
}
let start = needle_start - haystack_start;
let end = start + needle.len();
Some((start, end))
}
fn name_val_decoded(
name: &str,
val: &str,
) -> Result<(CookieStr, CookieStr), ParseError> {
let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?;
let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?;
let name = CookieStr::Concrete(Cow::Owned(decoded_name.into_owned()));
let val = CookieStr::Concrete(Cow::Owned(decoded_value.into_owned()));
Ok((name, val))
}
// This function does the real parsing but _does not_ set the `cookie_string` in
// the returned cookie object. This only exists so that the borrow to `s` is
// returned at the end of the call, allowing the `cookie_string` field to be
// set in the outer `parse` function.
fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
let mut attributes = s.split(';');
let key_value = match attributes.next() {
Some(s) => s,
_ => panic!(),
};
// Determine the name = val.
let (name, value) = match key_value.find('=') {
Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()),
None => return Err(ParseError::MissingPair),
};
if name.is_empty() {
return Err(ParseError::EmptyName);
}
// Create a cookie with all of the defaults. We'll fill things in while we
// iterate through the parameters below.
let (name, value) = if decode {
name_val_decoded(name, value)?
} else {
let name_indexes = indexes_of(name, s).expect("name sub");
let value_indexes = indexes_of(value, s).expect("value sub");
let name = CookieStr::Indexed(name_indexes.0, name_indexes.1);
let value = CookieStr::Indexed(value_indexes.0, value_indexes.1);
(name, value)
};
let mut cookie = Cookie {
name,
value,
cookie_string: None,
expires: None,
max_age: None,
domain: None,
path: None,
secure: None,
http_only: None,
same_site: None,
};
for attr in attributes {
let (key, value) = match attr.find('=') {
Some(i) => (attr[..i].trim(), Some(attr[(i + 1)..].trim())),
None => (attr.trim(), None),
};
match (&*key.to_ascii_lowercase(), value) {
("secure", _) => cookie.secure = Some(true),
("httponly", _) => cookie.http_only = Some(true),
("max-age", Some(v)) => {
// See RFC 6265 Section 5.2.2, negative values indicate that the
// earliest possible expiration time should be used, so set the
// max age as 0 seconds.
cookie.max_age = match v.parse() {
Ok(val) if val <= 0 => Some(Duration::zero()),
Ok(val) => {
// Don't panic if the max age seconds is greater than what's supported by
// `Duration`.
let val = cmp::min(val, Duration::max_value().num_seconds());
Some(Duration::seconds(val))
}
Err(_) => continue,
};
}
("domain", Some(mut domain)) if !domain.is_empty() => {
if domain.starts_with('.') {
domain = &domain[1..];
}
let (i, j) = indexes_of(domain, s).expect("domain sub");
cookie.domain = Some(CookieStr::Indexed(i, j));
}
("path", Some(v)) => {
let (i, j) = indexes_of(v, s).expect("path sub");
cookie.path = Some(CookieStr::Indexed(i, j));
}
("samesite", Some(v)) => {
if v.eq_ignore_ascii_case("strict") {
cookie.same_site = Some(SameSite::Strict);
} else if v.eq_ignore_ascii_case("lax") {
cookie.same_site = Some(SameSite::Lax);
} else {
// We do nothing here, for now. When/if the `SameSite`
// attribute becomes standard, the spec says that we should
// ignore this cookie, i.e, fail to parse it, when an
// invalid value is passed in. The draft is at
// http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html.
}
}
("expires", Some(v)) => {
// Try strptime with three date formats according to
// http://tools.ietf.org/html/rfc2616#section-3.3.1. Try
// additional ones as encountered in the real world.
let tm = time::strptime(v, "%a, %d %b %Y %H:%M:%S %Z")
.or_else(|_| time::strptime(v, "%A, %d-%b-%y %H:%M:%S %Z"))
.or_else(|_| time::strptime(v, "%a, %d-%b-%Y %H:%M:%S %Z"))
.or_else(|_| time::strptime(v, "%a %b %d %H:%M:%S %Y"));
if let Ok(time) = tm {
cookie.expires = Some(time)
}
}
_ => {
// We're going to be permissive here. If we have no idea what
// this is, then it's something nonstandard. We're not going to
// store it (because it's not compliant), but we're also not
// going to emit an error.
}
}
}
Ok(cookie)
}
pub fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result<Cookie<'c>, ParseError>
where
S: Into<Cow<'c, str>>,
{
let s = cow.into();
let mut cookie = parse_inner(&s, decode)?;
cookie.cookie_string = Some(s);
Ok(cookie)
}
#[cfg(test)]
mod tests {
use super::{Cookie, SameSite};
use chrono::Duration;
use time::strptime;
macro_rules! assert_eq_parse {
($string:expr, $expected:expr) => {
let cookie = match Cookie::parse($string) {
Ok(cookie) => cookie,
Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e),
};
assert_eq!(cookie, $expected);
};
}
macro_rules! assert_ne_parse {
($string:expr, $expected:expr) => {
let cookie = match Cookie::parse($string) {
Ok(cookie) => cookie,
Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e),
};
assert_ne!(cookie, $expected);
};
}
#[test]
fn parse_same_site() {
let expected = Cookie::build("foo", "bar")
.same_site(SameSite::Lax)
.finish();
assert_eq_parse!("foo=bar; SameSite=Lax", expected);
assert_eq_parse!("foo=bar; SameSite=lax", expected);
assert_eq_parse!("foo=bar; SameSite=LAX", expected);
assert_eq_parse!("foo=bar; samesite=Lax", expected);
assert_eq_parse!("foo=bar; SAMESITE=Lax", expected);
let expected = Cookie::build("foo", "bar")
.same_site(SameSite::Strict)
.finish();
assert_eq_parse!("foo=bar; SameSite=Strict", expected);
assert_eq_parse!("foo=bar; SameSITE=Strict", expected);
assert_eq_parse!("foo=bar; SameSite=strict", expected);
assert_eq_parse!("foo=bar; SameSite=STrICT", expected);
assert_eq_parse!("foo=bar; SameSite=STRICT", expected);
}
#[test]
fn parse() {
assert!(Cookie::parse("bar").is_err());
assert!(Cookie::parse("=bar").is_err());
assert!(Cookie::parse(" =bar").is_err());
assert!(Cookie::parse("foo=").is_ok());
let expected = Cookie::build("foo", "bar=baz").finish();
assert_eq_parse!("foo=bar=baz", expected);
let mut expected = Cookie::build("foo", "bar").finish();
assert_eq_parse!("foo=bar", expected);
assert_eq_parse!("foo = bar", expected);
assert_eq_parse!(" foo=bar ", expected);
assert_eq_parse!(" foo=bar ;Domain=", expected);
assert_eq_parse!(" foo=bar ;Domain= ", expected);
assert_eq_parse!(" foo=bar ;Ignored", expected);
let mut unexpected = Cookie::build("foo", "bar").http_only(false).finish();
assert_ne_parse!(" foo=bar ;HttpOnly", unexpected);
assert_ne_parse!(" foo=bar; httponly", unexpected);
expected.set_http_only(true);
assert_eq_parse!(" foo=bar ;HttpOnly", expected);
assert_eq_parse!(" foo=bar ;httponly", expected);
assert_eq_parse!(" foo=bar ;HTTPONLY=whatever", expected);
assert_eq_parse!(" foo=bar ; sekure; HTTPONLY", expected);
expected.set_secure(true);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure=aaaa", expected);
unexpected.set_http_only(true);
unexpected.set_secure(true);
assert_ne_parse!(" foo=bar ;HttpOnly; skeure", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; =secure", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly;", unexpected);
unexpected.set_secure(false);
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
expected.set_max_age(Duration::zero());
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected);
expected.set_max_age(Duration::minutes(1));
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 60 ", expected);
expected.set_max_age(Duration::seconds(4));
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 4 ", expected);
unexpected.set_secure(true);
unexpected.set_max_age(Duration::minutes(1));
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=122", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 38 ", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=51", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0", unexpected);
expected.set_path("/");
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/", expected);
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/", expected);
expected.set_path("/foo");
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", expected);
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/foo", expected);
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path=/foo", expected);
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path = /foo", expected);
unexpected.set_max_age(Duration::seconds(4));
unexpected.set_path("/bar");
assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", unexpected);
assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/baz", unexpected);
expected.set_domain("www.foo.com");
assert_eq_parse!(
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=www.foo.com",
expected
);
expected.set_domain("foo.com");
assert_eq_parse!(
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=foo.com",
expected
);
assert_eq_parse!(
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=FOO.COM",
expected
);
unexpected.set_path("/foo");
unexpected.set_domain("bar.com");
assert_ne_parse!(
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=foo.com",
unexpected
);
assert_ne_parse!(
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=FOO.COM",
unexpected
);
let time_str = "Wed, 21 Oct 2015 07:28:00 GMT";
let expires = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap();
expected.set_expires(expires);
assert_eq_parse!(
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
expected
);
unexpected.set_domain("foo.com");
let bad_expires = strptime(time_str, "%a, %d %b %Y %H:%S:%M %Z").unwrap();
expected.set_expires(bad_expires);
assert_ne_parse!(
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
unexpected
);
}
#[test]
fn odd_characters() {
let expected = Cookie::new("foo", "b%2Fr");
assert_eq_parse!("foo=b%2Fr", expected);
}
#[test]
fn odd_characters_encoded() {
let expected = Cookie::new("foo", "b/r");
let cookie = match Cookie::parse_encoded("foo=b%2Fr") {
Ok(cookie) => cookie,
Err(e) => panic!("Failed to parse: {:?}", e),
};
assert_eq!(cookie, expected);
}
#[test]
fn do_not_panic_on_large_max_ages() {
let max_seconds = Duration::max_value().num_seconds();
let expected = Cookie::build("foo", "bar").max_age(max_seconds).finish();
assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", max_seconds + 1), expected);
}
}

View File

@ -0,0 +1,190 @@
use ring::hkdf::{Algorithm, KeyType, Prk, HKDF_SHA256};
use ring::rand::{SecureRandom, SystemRandom};
use super::private::KEY_LEN as PRIVATE_KEY_LEN;
use super::signed::KEY_LEN as SIGNED_KEY_LEN;
static HKDF_DIGEST: Algorithm = HKDF_SHA256;
const KEYS_INFO: &[&[u8]] = &[b"COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM"];
/// A cryptographic master key for use with `Signed` and/or `Private` jars.
///
/// This structure encapsulates secure, cryptographic keys for use with both
/// [PrivateJar](struct.PrivateJar.html) and [SignedJar](struct.SignedJar.html).
/// It can be derived from a single master key via
/// [from_master](#method.from_master) or generated from a secure random source
/// via [generate](#method.generate). A single instance of `Key` can be used for
/// both a `PrivateJar` and a `SignedJar`.
///
/// This type is only available when the `secure` feature is enabled.
#[derive(Clone)]
pub struct Key {
signing_key: [u8; SIGNED_KEY_LEN],
encryption_key: [u8; PRIVATE_KEY_LEN],
}
impl KeyType for &Key {
#[inline]
fn len(&self) -> usize {
SIGNED_KEY_LEN + PRIVATE_KEY_LEN
}
}
impl Key {
/// Derives new signing/encryption keys from a master key.
///
/// The master key must be at least 256-bits (32 bytes). For security, the
/// master key _must_ be cryptographically random. The keys are derived
/// deterministically from the master key.
///
/// # Panics
///
/// Panics if `key` is less than 32 bytes in length.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Key;
///
/// # /*
/// let master_key = { /* a cryptographically random key >= 32 bytes */ };
/// # */
/// # let master_key: &Vec<u8> = &(0..32).collect();
///
/// let key = Key::from_master(master_key);
/// ```
pub fn from_master(key: &[u8]) -> Key {
if key.len() < 32 {
panic!(
"bad master key length: expected at least 32 bytes, found {}",
key.len()
);
}
// An empty `Key` structure; will be filled in with HKDF derived keys.
let mut output_key = Key {
signing_key: [0; SIGNED_KEY_LEN],
encryption_key: [0; PRIVATE_KEY_LEN],
};
// Expand the master key into two HKDF generated keys.
let mut both_keys = [0; SIGNED_KEY_LEN + PRIVATE_KEY_LEN];
let prk = Prk::new_less_safe(HKDF_DIGEST, key);
let okm = prk.expand(KEYS_INFO, &output_key).expect("okm expand");
okm.fill(&mut both_keys).expect("fill keys");
// Copy the key parts into their respective fields.
output_key
.signing_key
.copy_from_slice(&both_keys[..SIGNED_KEY_LEN]);
output_key
.encryption_key
.copy_from_slice(&both_keys[SIGNED_KEY_LEN..]);
output_key
}
/// Generates signing/encryption keys from a secure, random source. Keys are
/// generated nondeterministically.
///
/// # Panics
///
/// Panics if randomness cannot be retrieved from the operating system. See
/// [try_generate](#method.try_generate) for a non-panicking version.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Key;
///
/// let key = Key::generate();
/// ```
pub fn generate() -> Key {
Self::try_generate().expect("failed to generate `Key` from randomness")
}
/// Attempts to generate signing/encryption keys from a secure, random
/// source. Keys are generated nondeterministically. If randomness cannot be
/// retrieved from the underlying operating system, returns `None`.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Key;
///
/// let key = Key::try_generate();
/// ```
pub fn try_generate() -> Option<Key> {
let mut sign_key = [0; SIGNED_KEY_LEN];
let mut enc_key = [0; PRIVATE_KEY_LEN];
let rng = SystemRandom::new();
if rng.fill(&mut sign_key).is_err() || rng.fill(&mut enc_key).is_err() {
return None;
}
Some(Key {
signing_key: sign_key,
encryption_key: enc_key,
})
}
/// Returns the raw bytes of a key suitable for signing cookies.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Key;
///
/// let key = Key::generate();
/// let signing_key = key.signing();
/// ```
pub fn signing(&self) -> &[u8] {
&self.signing_key[..]
}
/// Returns the raw bytes of a key suitable for encrypting cookies.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::Key;
///
/// let key = Key::generate();
/// let encryption_key = key.encryption();
/// ```
pub fn encryption(&self) -> &[u8] {
&self.encryption_key[..]
}
}
#[cfg(test)]
mod test {
use super::Key;
#[test]
fn deterministic_from_master() {
let master_key: Vec<u8> = (0..32).collect();
let key_a = Key::from_master(&master_key);
let key_b = Key::from_master(&master_key);
assert_eq!(key_a.signing(), key_b.signing());
assert_eq!(key_a.encryption(), key_b.encryption());
assert_ne!(key_a.encryption(), key_a.signing());
let master_key_2: Vec<u8> = (32..64).collect();
let key_2 = Key::from_master(&master_key_2);
assert_ne!(key_2.signing(), key_a.signing());
assert_ne!(key_2.encryption(), key_a.encryption());
}
#[test]
fn non_deterministic_generate() {
let key_a = Key::generate();
let key_b = Key::generate();
assert_ne!(key_a.signing(), key_b.signing());
assert_ne!(key_a.encryption(), key_b.encryption());
}
}

View File

@ -0,0 +1,40 @@
#[cfg(test)]
macro_rules! assert_simple_behaviour {
($clear:expr, $secure:expr) => {{
assert_eq!($clear.iter().count(), 0);
$secure.add(Cookie::new("name", "val"));
assert_eq!($clear.iter().count(), 1);
assert_eq!($secure.get("name").unwrap().value(), "val");
assert_ne!($clear.get("name").unwrap().value(), "val");
$secure.add(Cookie::new("another", "two"));
assert_eq!($clear.iter().count(), 2);
$clear.remove(Cookie::named("another"));
assert_eq!($clear.iter().count(), 1);
$secure.remove(Cookie::named("name"));
assert_eq!($clear.iter().count(), 0);
}};
}
#[cfg(test)]
macro_rules! assert_secure_behaviour {
($clear:expr, $secure:expr) => {{
$secure.add(Cookie::new("secure", "secure"));
assert!($clear.get("secure").unwrap().value() != "secure");
assert!($secure.get("secure").unwrap().value() == "secure");
let mut cookie = $clear.get("secure").unwrap().clone();
let new_val = format!("{}l", cookie.value());
cookie.set_value(new_val);
$clear.add(cookie);
assert!($secure.get("secure").is_none());
let mut cookie = $clear.get("secure").unwrap().clone();
cookie.set_value("foobar");
$clear.add(cookie);
assert!($secure.get("secure").is_none());
}};
}

View File

@ -0,0 +1,10 @@
//! Fork of https://github.com/alexcrichton/cookie-rs
#[macro_use]
mod macros;
mod key;
mod private;
mod signed;
pub use self::key::*;
pub use self::private::*;
pub use self::signed::*;

View File

@ -0,0 +1,275 @@
use std::str;
use log::warn;
use ring::aead::{Aad, Algorithm, Nonce, AES_256_GCM};
use ring::aead::{LessSafeKey, UnboundKey};
use ring::rand::{SecureRandom, SystemRandom};
use super::Key;
use crate::cookie::{Cookie, CookieJar};
// Keep these in sync, and keep the key len synced with the `private` docs as
// well as the `KEYS_INFO` const in secure::Key.
static ALGO: &Algorithm = &AES_256_GCM;
const NONCE_LEN: usize = 12;
pub const KEY_LEN: usize = 32;
/// A child cookie jar that provides authenticated encryption for its cookies.
///
/// A _private_ child jar signs and encrypts all the cookies added to it and
/// verifies and decrypts cookies retrieved from it. Any cookies stored in a
/// `PrivateJar` are simultaneously assured confidentiality, integrity, and
/// authenticity. In other words, clients cannot discover nor tamper with the
/// contents of a cookie, nor can they fabricate cookie data.
///
/// This type is only available when the `secure` feature is enabled.
pub struct PrivateJar<'a> {
parent: &'a mut CookieJar,
key: [u8; KEY_LEN],
}
impl<'a> PrivateJar<'a> {
/// Creates a new child `PrivateJar` with parent `parent` and key `key`.
/// This method is typically called indirectly via the `signed` method of
/// `CookieJar`.
#[doc(hidden)]
pub fn new(parent: &'a mut CookieJar, key: &Key) -> PrivateJar<'a> {
let mut key_array = [0u8; KEY_LEN];
key_array.copy_from_slice(key.encryption());
PrivateJar {
parent,
key: key_array,
}
}
/// Given a sealed value `str` and a key name `name`, where the nonce is
/// prepended to the original value and then both are Base64 encoded,
/// verifies and decrypts the sealed value and returns it. If there's a
/// problem, returns an `Err` with a string describing the issue.
fn unseal(&self, name: &str, value: &str) -> Result<String, &'static str> {
let mut data = base64::decode(value).map_err(|_| "bad base64 value")?;
if data.len() <= NONCE_LEN {
return Err("length of decoded data is <= NONCE_LEN");
}
let ad = Aad::from(name.as_bytes());
let key = LessSafeKey::new(
UnboundKey::new(&ALGO, &self.key).expect("matching key length"),
);
let (nonce, mut sealed) = data.split_at_mut(NONCE_LEN);
let nonce =
Nonce::try_assume_unique_for_key(nonce).expect("invalid length of `nonce`");
let unsealed = key
.open_in_place(nonce, ad, &mut sealed)
.map_err(|_| "invalid key/nonce/value: bad seal")?;
if let Ok(unsealed_utf8) = str::from_utf8(unsealed) {
Ok(unsealed_utf8.to_string())
} else {
warn!(
"Private cookie does not have utf8 content!
It is likely the secret key used to encrypt them has been leaked.
Please change it as soon as possible."
);
Err("bad unsealed utf8")
}
}
/// Returns a reference to the `Cookie` inside this jar with the name `name`
/// and authenticates and decrypts the cookie's value, returning a `Cookie`
/// with the decrypted value. If the cookie cannot be found, or the cookie
/// fails to authenticate or decrypt, `None` is returned.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// let mut private_jar = jar.private(&key);
/// assert!(private_jar.get("name").is_none());
///
/// private_jar.add(Cookie::new("name", "value"));
/// assert_eq!(private_jar.get("name").unwrap().value(), "value");
/// ```
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
if let Some(cookie_ref) = self.parent.get(name) {
let mut cookie = cookie_ref.clone();
if let Ok(value) = self.unseal(name, cookie.value()) {
cookie.set_value(value);
return Some(cookie);
}
}
None
}
/// Adds `cookie` to the parent jar. The cookie's value is encrypted with
/// authenticated encryption assuring confidentiality, integrity, and
/// authenticity.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// jar.private(&key).add(Cookie::new("name", "value"));
///
/// assert_ne!(jar.get("name").unwrap().value(), "value");
/// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value");
/// ```
pub fn add(&mut self, mut cookie: Cookie<'static>) {
self.encrypt_cookie(&mut cookie);
// Add the sealed cookie to the parent.
self.parent.add(cookie);
}
/// Adds an "original" `cookie` to parent jar. The cookie's value is
/// encrypted with authenticated encryption assuring confidentiality,
/// integrity, and authenticity. Adding an original cookie does not affect
/// the [`CookieJar::delta()`](struct.CookieJar.html#method.delta)
/// computation. This method is intended to be used to seed the cookie jar
/// with cookies received from a client's HTTP message.
///
/// For accurate `delta` computations, this method should not be called
/// after calling `remove`.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// jar.private(&key).add_original(Cookie::new("name", "value"));
///
/// assert_eq!(jar.iter().count(), 1);
/// assert_eq!(jar.delta().count(), 0);
/// ```
pub fn add_original(&mut self, mut cookie: Cookie<'static>) {
self.encrypt_cookie(&mut cookie);
// Add the sealed cookie to the parent.
self.parent.add_original(cookie);
}
/// Encrypts the cookie's value with
/// authenticated encryption assuring confidentiality, integrity, and authenticity.
fn encrypt_cookie(&self, cookie: &mut Cookie<'_>) {
let name = cookie.name().as_bytes();
let value = cookie.value().as_bytes();
let data = encrypt_name_value(name, value, &self.key);
// Base64 encode the nonce and encrypted value.
let sealed_value = base64::encode(&data);
cookie.set_value(sealed_value);
}
/// Removes `cookie` from the parent jar.
///
/// For correct removal, the passed in `cookie` must contain the same `path`
/// and `domain` as the cookie that was initially set.
///
/// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more
/// details.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// let mut private_jar = jar.private(&key);
///
/// private_jar.add(Cookie::new("name", "value"));
/// assert!(private_jar.get("name").is_some());
///
/// private_jar.remove(Cookie::named("name"));
/// assert!(private_jar.get("name").is_none());
/// ```
pub fn remove(&mut self, cookie: Cookie<'static>) {
self.parent.remove(cookie);
}
}
fn encrypt_name_value(name: &[u8], value: &[u8], key: &[u8]) -> Vec<u8> {
// Create the `SealingKey` structure.
let unbound = UnboundKey::new(&ALGO, key).expect("matching key length");
let key = LessSafeKey::new(unbound);
// Create a vec to hold the [nonce | cookie value | overhead].
let mut data = vec![0; NONCE_LEN + value.len() + ALGO.tag_len()];
// Randomly generate the nonce, then copy the cookie value as input.
let (nonce, in_out) = data.split_at_mut(NONCE_LEN);
let (in_out, tag) = in_out.split_at_mut(value.len());
in_out.copy_from_slice(value);
// Randomly generate the nonce into the nonce piece.
SystemRandom::new()
.fill(nonce)
.expect("couldn't random fill nonce");
let nonce = Nonce::try_assume_unique_for_key(nonce).expect("invalid `nonce` length");
// Use cookie's name as associated data to prevent value swapping.
let ad = Aad::from(name);
let ad_tag = key
.seal_in_place_separate_tag(nonce, ad, in_out)
.expect("in-place seal");
// Copy the tag into the tag piece.
tag.copy_from_slice(ad_tag.as_ref());
// Remove the overhead and return the sealed content.
data
}
#[cfg(test)]
mod test {
use super::{encrypt_name_value, Cookie, CookieJar, Key};
#[test]
fn simple() {
let key = Key::generate();
let mut jar = CookieJar::new();
assert_simple_behaviour!(jar, jar.private(&key));
}
#[test]
fn private() {
let key = Key::generate();
let mut jar = CookieJar::new();
assert_secure_behaviour!(jar, jar.private(&key));
}
#[test]
fn non_utf8() {
let key = Key::generate();
let mut jar = CookieJar::new();
let name = "malicious";
let mut assert_non_utf8 = |value: &[u8]| {
let sealed = encrypt_name_value(name.as_bytes(), value, &key.encryption());
let encoded = base64::encode(&sealed);
assert_eq!(
jar.private(&key).unseal(name, &encoded),
Err("bad unsealed utf8")
);
jar.add(Cookie::new(name, encoded));
assert_eq!(jar.private(&key).get(name), None);
};
assert_non_utf8(&[0x72, 0xfb, 0xdf, 0x74]); // rûst in ISO/IEC 8859-1
let mut malicious =
String::from(r#"{"id":"abc123??%X","admin":true}"#).into_bytes();
malicious[8] |= 0b1100_0000;
malicious[9] |= 0b1100_0000;
assert_non_utf8(&malicious);
}
}

View File

@ -0,0 +1,184 @@
use ring::hmac::{self, sign, verify};
use super::Key;
use crate::cookie::{Cookie, CookieJar};
// Keep these in sync, and keep the key len synced with the `signed` docs as
// well as the `KEYS_INFO` const in secure::Key.
static HMAC_DIGEST: hmac::Algorithm = hmac::HMAC_SHA256;
const BASE64_DIGEST_LEN: usize = 44;
pub const KEY_LEN: usize = 32;
/// A child cookie jar that authenticates its cookies.
///
/// A _signed_ child jar signs all the cookies added to it and verifies cookies
/// retrieved from it. Any cookies stored in a `SignedJar` are assured integrity
/// and authenticity. In other words, clients cannot tamper with the contents of
/// a cookie nor can they fabricate cookie values, but the data is visible in
/// plaintext.
///
/// This type is only available when the `secure` feature is enabled.
pub struct SignedJar<'a> {
parent: &'a mut CookieJar,
key: hmac::Key,
}
impl<'a> SignedJar<'a> {
/// Creates a new child `SignedJar` with parent `parent` and key `key`. This
/// method is typically called indirectly via the `signed` method of
/// `CookieJar`.
#[doc(hidden)]
pub fn new(parent: &'a mut CookieJar, key: &Key) -> SignedJar<'a> {
SignedJar {
parent,
key: hmac::Key::new(HMAC_DIGEST, key.signing()),
}
}
/// Given a signed value `str` where the signature is prepended to `value`,
/// verifies the signed value and returns it. If there's a problem, returns
/// an `Err` with a string describing the issue.
fn verify(&self, cookie_value: &str) -> Result<String, &'static str> {
if cookie_value.len() < BASE64_DIGEST_LEN {
return Err("length of value is <= BASE64_DIGEST_LEN");
}
let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
let sig = base64::decode(digest_str).map_err(|_| "bad base64 digest")?;
verify(&self.key, value.as_bytes(), &sig)
.map(|_| value.to_string())
.map_err(|_| "value did not verify")
}
/// Returns a reference to the `Cookie` inside this jar with the name `name`
/// and verifies the authenticity and integrity of the cookie's value,
/// returning a `Cookie` with the authenticated value. If the cookie cannot
/// be found, or the cookie fails to verify, `None` is returned.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// let mut signed_jar = jar.signed(&key);
/// assert!(signed_jar.get("name").is_none());
///
/// signed_jar.add(Cookie::new("name", "value"));
/// assert_eq!(signed_jar.get("name").unwrap().value(), "value");
/// ```
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
if let Some(cookie_ref) = self.parent.get(name) {
let mut cookie = cookie_ref.clone();
if let Ok(value) = self.verify(cookie.value()) {
cookie.set_value(value);
return Some(cookie);
}
}
None
}
/// Adds `cookie` to the parent jar. The cookie's value is signed assuring
/// integrity and authenticity.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// jar.signed(&key).add(Cookie::new("name", "value"));
///
/// assert_ne!(jar.get("name").unwrap().value(), "value");
/// assert!(jar.get("name").unwrap().value().contains("value"));
/// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value");
/// ```
pub fn add(&mut self, mut cookie: Cookie<'static>) {
self.sign_cookie(&mut cookie);
self.parent.add(cookie);
}
/// Adds an "original" `cookie` to this jar. The cookie's value is signed
/// assuring integrity and authenticity. Adding an original cookie does not
/// affect the [`CookieJar::delta()`](struct.CookieJar.html#method.delta)
/// computation. This method is intended to be used to seed the cookie jar
/// with cookies received from a client's HTTP message.
///
/// For accurate `delta` computations, this method should not be called
/// after calling `remove`.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// jar.signed(&key).add_original(Cookie::new("name", "value"));
///
/// assert_eq!(jar.iter().count(), 1);
/// assert_eq!(jar.delta().count(), 0);
/// ```
pub fn add_original(&mut self, mut cookie: Cookie<'static>) {
self.sign_cookie(&mut cookie);
self.parent.add_original(cookie);
}
/// Signs the cookie's value assuring integrity and authenticity.
fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
let digest = sign(&self.key, cookie.value().as_bytes());
let mut new_value = base64::encode(digest.as_ref());
new_value.push_str(cookie.value());
cookie.set_value(new_value);
}
/// Removes `cookie` from the parent jar.
///
/// For correct removal, the passed in `cookie` must contain the same `path`
/// and `domain` as the cookie that was initially set.
///
/// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more
/// details.
///
/// # Example
///
/// ```rust
/// use actix_http::cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// let mut signed_jar = jar.signed(&key);
///
/// signed_jar.add(Cookie::new("name", "value"));
/// assert!(signed_jar.get("name").is_some());
///
/// signed_jar.remove(Cookie::named("name"));
/// assert!(signed_jar.get("name").is_none());
/// ```
pub fn remove(&mut self, cookie: Cookie<'static>) {
self.parent.remove(cookie);
}
}
#[cfg(test)]
mod test {
use super::{Cookie, CookieJar, Key};
#[test]
fn simple() {
let key = Key::generate();
let mut jar = CookieJar::new();
assert_simple_behaviour!(jar, jar.signed(&key));
}
#[test]
fn private() {
let key = Key::generate();
let mut jar = CookieJar::new();
assert_secure_behaviour!(jar, jar.signed(&key));
}
}

View File

@ -1,17 +1,20 @@
use std::future::Future;
use std::io::{self, Write}; use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_threadpool::{run, CpuFuture}; use actix_threadpool::{run, CpuFuture};
#[cfg(feature = "brotli")]
use brotli2::write::BrotliDecoder; use brotli2::write::BrotliDecoder;
use bytes::Bytes; use bytes::Bytes;
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
use flate2::write::{GzDecoder, ZlibDecoder}; use flate2::write::{GzDecoder, ZlibDecoder};
use futures::{try_ready, Async, Future, Poll, Stream}; use futures_core::{ready, Stream};
use super::Writer; use super::Writer;
use crate::error::PayloadError; use crate::error::PayloadError;
use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}; use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING};
const INPLACE: usize = 2049;
pub struct Decoder<S> { pub struct Decoder<S> {
decoder: Option<ContentDecoder>, decoder: Option<ContentDecoder>,
stream: S, stream: S,
@ -21,21 +24,18 @@ pub struct Decoder<S> {
impl<S> Decoder<S> impl<S> Decoder<S>
where where
S: Stream<Item = Bytes, Error = PayloadError>, S: Stream<Item = Result<Bytes, PayloadError>>,
{ {
/// Construct a decoder. /// Construct a decoder.
#[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 = "brotli")]
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(
BrotliDecoder::new(Writer::new()), BrotliDecoder::new(Writer::new()),
))), ))),
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
ZlibDecoder::new(Writer::new()), ZlibDecoder::new(Writer::new()),
))), ))),
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(
GzDecoder::new(Writer::new()), GzDecoder::new(Writer::new()),
))), ))),
@ -53,7 +53,7 @@ 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 = if let Some(enc) = headers.get(&CONTENT_ENCODING) {
if let Ok(enc) = enc.to_str() { if let Ok(enc) = enc.to_str() {
ContentEncoding::from(enc) ContentEncoding::from(enc)
} else { } else {
@ -69,74 +69,83 @@ where
impl<S> Stream for Decoder<S> impl<S> Stream for Decoder<S>
where where
S: Stream<Item = Bytes, Error = PayloadError>, S: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
{ {
type Item = Bytes; type Item = Result<Bytes, PayloadError>;
type Error = PayloadError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> 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) = try_ready!(fut.poll()); let (chunk, decoder) = match ready!(Pin::new(fut).poll(cx)) {
Ok(item) => item,
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 Ok(Async::Ready(Some(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
if self.eof { if self.eof {
return Ok(Async::Ready(None)); return Poll::Ready(None);
} }
match self.stream.poll()? { match Pin::new(&mut self.stream).poll_next(cx) {
Async::Ready(Some(chunk)) => { Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
Poll::Ready(Some(Ok(chunk))) => {
if let Some(mut decoder) = self.decoder.take() { if let Some(mut decoder) = self.decoder.take() {
self.fut = Some(run(move || { if chunk.len() < INPLACE {
let chunk = decoder.feed_data(chunk)?; let chunk = decoder.feed_data(chunk)?;
Ok((chunk, decoder)) self.decoder = Some(decoder);
})); if let Some(chunk) = chunk {
return Poll::Ready(Some(Ok(chunk)));
}
} else {
self.fut = Some(run(move || {
let chunk = decoder.feed_data(chunk)?;
Ok((chunk, decoder))
}));
}
continue; continue;
} else { } else {
return Ok(Async::Ready(Some(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
Async::Ready(None) => { Poll::Ready(None) => {
self.eof = true; self.eof = true;
if let Some(mut decoder) = self.decoder.take() { return if let Some(mut decoder) = self.decoder.take() {
self.fut = Some(run(move || { match decoder.feed_eof() {
let chunk = decoder.feed_eof()?; Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
Ok((chunk, decoder)) Ok(None) => Poll::Ready(None),
})); Err(err) => Poll::Ready(Some(Err(err.into()))),
continue; }
} else { } else {
return Ok(Async::Ready(None)); Poll::Ready(None)
}; };
} }
Async::NotReady => break, Poll::Pending => break,
} }
} }
Ok(Async::NotReady) Poll::Pending
} }
} }
enum ContentDecoder { enum ContentDecoder {
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
Deflate(Box<ZlibDecoder<Writer>>), Deflate(Box<ZlibDecoder<Writer>>),
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
Gzip(Box<GzDecoder<Writer>>), Gzip(Box<GzDecoder<Writer>>),
#[cfg(feature = "brotli")]
Br(Box<BrotliDecoder<Writer>>), Br(Box<BrotliDecoder<Writer>>),
} }
impl ContentDecoder { impl ContentDecoder {
#[allow(unreachable_patterns)]
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> { fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
match self { match self {
#[cfg(feature = "brotli")] ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
ContentDecoder::Br(ref mut decoder) => match decoder.finish() { Ok(()) => {
Ok(mut writer) => { let b = decoder.get_mut().take();
let b = writer.take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -145,7 +154,6 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
@ -157,7 +165,6 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
@ -169,14 +176,11 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
_ => Ok(None),
} }
} }
#[allow(unreachable_patterns)]
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 = "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()?;
@ -189,7 +193,6 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
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()?;
@ -202,7 +205,6 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
@ -215,7 +217,6 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
_ => Ok(Some(data)),
} }
} }
} }

View File

@ -1,24 +1,29 @@
//! Stream encoder //! Stream encoder
use std::future::Future;
use std::io::{self, Write}; use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::Bytes; use actix_threadpool::{run, CpuFuture};
use futures::{Async, Poll};
#[cfg(feature = "brotli")]
use brotli2::write::BrotliEncoder; use brotli2::write::BrotliEncoder;
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] use bytes::Bytes;
use flate2::write::{GzEncoder, ZlibEncoder}; use flate2::write::{GzEncoder, ZlibEncoder};
use futures_core::ready;
use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::body::{Body, BodySize, MessageBody, ResponseBody};
use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; use crate::http::header::{ContentEncoding, CONTENT_ENCODING};
use crate::http::{HeaderValue, HttpTryFrom, StatusCode}; use crate::http::{HeaderValue, StatusCode};
use crate::{Error, ResponseHead}; use crate::{Error, ResponseHead};
use super::Writer; use super::Writer;
const INPLACE: usize = 1024;
pub struct Encoder<B> { pub struct Encoder<B> {
eof: bool,
body: EncoderBody<B>, body: EncoderBody<B>,
encoder: Option<ContentEncoder>, encoder: Option<ContentEncoder>,
fut: Option<CpuFuture<ContentEncoder, io::Error>>,
} }
impl<B: MessageBody> Encoder<B> { impl<B: MessageBody> Encoder<B> {
@ -27,108 +32,133 @@ impl<B: MessageBody> Encoder<B> {
head: &mut ResponseHead, head: &mut ResponseHead,
body: ResponseBody<B>, body: ResponseBody<B>,
) -> ResponseBody<Encoder<B>> { ) -> ResponseBody<Encoder<B>> {
let has_ce = head.headers().contains_key(CONTENT_ENCODING); let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
match body { || head.status == StatusCode::SWITCHING_PROTOCOLS
ResponseBody::Other(b) => match b { || head.status == StatusCode::NO_CONTENT
Body::None => ResponseBody::Other(Body::None), || encoding == ContentEncoding::Identity
Body::Empty => ResponseBody::Other(Body::Empty), || encoding == ContentEncoding::Auto);
Body::Bytes(buf) => {
if !(has_ce
|| encoding == ContentEncoding::Identity
|| encoding == ContentEncoding::Auto)
{
let mut enc = ContentEncoder::encoder(encoding).unwrap();
// TODO return error! let body = match body {
let _ = enc.write(buf.as_ref()); ResponseBody::Other(b) => match b {
let body = enc.finish().unwrap(); Body::None => return ResponseBody::Other(Body::None),
update_head(encoding, head); Body::Empty => return ResponseBody::Other(Body::Empty),
ResponseBody::Other(Body::Bytes(body)) Body::Bytes(buf) => {
if can_encode {
EncoderBody::Bytes(buf)
} else { } else {
ResponseBody::Other(Body::Bytes(buf)) return ResponseBody::Other(Body::Bytes(buf));
}
}
Body::Message(stream) => {
if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS {
ResponseBody::Body(Encoder {
body: EncoderBody::Other(stream),
encoder: None,
})
} else {
update_head(encoding, head);
head.no_chunking(false);
ResponseBody::Body(Encoder {
body: EncoderBody::Other(stream),
encoder: ContentEncoder::encoder(encoding),
})
} }
} }
Body::Message(stream) => EncoderBody::BoxedStream(stream),
}, },
ResponseBody::Body(stream) => { ResponseBody::Body(stream) => EncoderBody::Stream(stream),
if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS { };
ResponseBody::Body(Encoder {
body: EncoderBody::Body(stream), if can_encode {
encoder: None, // Modify response body only if encoder is not None
}) if let Some(enc) = ContentEncoder::encoder(encoding) {
} else { update_head(encoding, head);
update_head(encoding, head); head.no_chunking(false);
head.no_chunking(false); return ResponseBody::Body(Encoder {
ResponseBody::Body(Encoder { body,
body: EncoderBody::Body(stream), eof: false,
encoder: ContentEncoder::encoder(encoding), fut: None,
}) encoder: Some(enc),
} });
} }
} }
ResponseBody::Body(Encoder {
body,
eof: false,
fut: None,
encoder: None,
})
} }
} }
enum EncoderBody<B> { enum EncoderBody<B> {
Body(B), Bytes(Bytes),
Other(Box<dyn MessageBody>), Stream(B),
BoxedStream(Box<dyn MessageBody>),
} }
impl<B: MessageBody> MessageBody for Encoder<B> { impl<B: MessageBody> MessageBody for Encoder<B> {
fn length(&self) -> BodySize { fn size(&self) -> BodySize {
if self.encoder.is_none() { if self.encoder.is_none() {
match self.body { match self.body {
EncoderBody::Body(ref b) => b.length(), EncoderBody::Bytes(ref b) => b.size(),
EncoderBody::Other(ref b) => b.length(), EncoderBody::Stream(ref b) => b.size(),
EncoderBody::BoxedStream(ref b) => b.size(),
} }
} else { } else {
BodySize::Stream BodySize::Stream
} }
} }
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> { fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
loop { loop {
if self.eof {
return Poll::Ready(None);
}
if let Some(ref mut fut) = self.fut {
let mut encoder = match ready!(Pin::new(fut).poll(cx)) {
Ok(item) => item,
Err(e) => return Poll::Ready(Some(Err(e.into()))),
};
let chunk = encoder.take();
self.encoder = Some(encoder);
self.fut.take();
if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk)));
}
}
let result = match self.body { let result = match self.body {
EncoderBody::Body(ref mut b) => b.poll_next()?, EncoderBody::Bytes(ref mut b) => {
EncoderBody::Other(ref mut b) => b.poll_next()?, if b.is_empty() {
}; Poll::Ready(None)
match result {
Async::NotReady => return Ok(Async::NotReady),
Async::Ready(Some(chunk)) => {
if let Some(ref mut encoder) = self.encoder {
if encoder.write(&chunk)? {
return Ok(Async::Ready(Some(encoder.take())));
}
} else { } else {
return Ok(Async::Ready(Some(chunk))); Poll::Ready(Some(Ok(std::mem::replace(b, Bytes::new()))))
} }
} }
Async::Ready(None) => { EncoderBody::Stream(ref mut b) => b.poll_next(cx),
EncoderBody::BoxedStream(ref mut b) => b.poll_next(cx),
};
match result {
Poll::Ready(Some(Ok(chunk))) => {
if let Some(mut encoder) = self.encoder.take() {
if chunk.len() < INPLACE {
encoder.write(&chunk)?;
let chunk = encoder.take();
self.encoder = Some(encoder);
if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk)));
}
} else {
self.fut = Some(run(move || {
encoder.write(&chunk)?;
Ok(encoder)
}));
}
} else {
return Poll::Ready(Some(Ok(chunk)));
}
}
Poll::Ready(None) => {
if let Some(encoder) = self.encoder.take() { if let Some(encoder) = self.encoder.take() {
let chunk = encoder.finish()?; let chunk = encoder.finish()?;
if chunk.is_empty() { if chunk.is_empty() {
return Ok(Async::Ready(None)); return Poll::Ready(None);
} else { } else {
return Ok(Async::Ready(Some(chunk))); self.eof = true;
return Poll::Ready(Some(Ok(chunk)));
} }
} else { } else {
return Ok(Async::Ready(None)); return Poll::Ready(None);
} }
} }
val => return val,
} }
} }
} }
@ -137,33 +167,27 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
head.headers_mut().insert( head.headers_mut().insert(
CONTENT_ENCODING, CONTENT_ENCODING,
HeaderValue::try_from(Bytes::from_static(encoding.as_str().as_bytes())).unwrap(), HeaderValue::from_static(encoding.as_str()),
); );
} }
enum ContentEncoder { enum ContentEncoder {
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
Deflate(ZlibEncoder<Writer>), Deflate(ZlibEncoder<Writer>),
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
Gzip(GzEncoder<Writer>), Gzip(GzEncoder<Writer>),
#[cfg(feature = "brotli")]
Br(BrotliEncoder<Writer>), Br(BrotliEncoder<Writer>),
} }
impl ContentEncoder { impl ContentEncoder {
fn encoder(encoding: ContentEncoding) -> Option<Self> { fn encoder(encoding: ContentEncoding) -> Option<Self> {
match encoding { match encoding {
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
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(any(feature = "flate2-zlib", feature = "flate2-rust"))]
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 = "brotli")]
ContentEncoding::Br => { ContentEncoding::Br => {
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
} }
@ -174,28 +198,22 @@ impl ContentEncoder {
#[inline] #[inline]
pub(crate) fn take(&mut self) -> Bytes { pub(crate) fn take(&mut self) -> Bytes {
match *self { match *self {
#[cfg(feature = "brotli")]
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Gzip(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 = "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(any(feature = "flate2-zlib", feature = "flate2-rust"))]
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(any(feature = "flate2-zlib", feature = "flate2-rust"))]
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),
@ -203,27 +221,24 @@ impl ContentEncoder {
} }
} }
fn write(&mut self, data: &[u8]) -> Result<bool, io::Error> { fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
match *self { match *self {
#[cfg(feature = "brotli")]
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
trace!("Error decoding br encoding: {}", err); trace!("Error decoding br encoding: {}", err);
Err(err) Err(err)
} }
}, },
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
trace!("Error decoding gzip encoding: {}", err); trace!("Error decoding gzip encoding: {}", err);
Err(err) Err(err)
} }
}, },
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
trace!("Error decoding deflate encoding: {}", err); trace!("Error decoding deflate encoding: {}", err);
Err(err) Err(err)

View File

@ -19,8 +19,9 @@ impl Writer {
buf: BytesMut::with_capacity(8192), buf: BytesMut::with_capacity(8192),
} }
} }
fn take(&mut self) -> Bytes { fn take(&mut self) -> Bytes {
self.buf.take().freeze() self.buf.split().freeze()
} }
} }
@ -29,6 +30,7 @@ impl io::Write for Writer {
self.buf.extend_from_slice(buf); self.buf.extend_from_slice(buf);
Ok(buf.len()) Ok(buf.len())
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
Ok(()) Ok(())
} }

View File

@ -1,30 +1,30 @@
//! Error and Result module //! Error and Result module
use std::any::TypeId;
use std::cell::RefCell; use std::cell::RefCell;
use std::io::Write;
use std::str::Utf8Error; use std::str::Utf8Error;
use std::string::FromUtf8Error; use std::string::FromUtf8Error;
use std::{fmt, io, result}; use std::{fmt, io, result};
// use actix::MailboxError; use actix_codec::{Decoder, Encoder};
pub use actix_threadpool::BlockingError; pub use actix_threadpool::BlockingError;
use actix_utils::framed::DispatcherError as FramedDispatcherError;
use actix_utils::timeout::TimeoutError; use actix_utils::timeout::TimeoutError;
#[cfg(feature = "cookies")] use bytes::BytesMut;
use cookie;
use derive_more::{Display, From}; use derive_more::{Display, From};
use futures::Canceled; pub use futures_channel::oneshot::Canceled;
use http::uri::InvalidUri; use http::uri::InvalidUri;
use http::{header, Error as HttpError, StatusCode}; use http::{header, Error as HttpError, StatusCode};
use httparse; use httparse;
use serde::de::value::Error as DeError; use serde::de::value::Error as DeError;
use serde_json::error::Error as JsonError; use serde_json::error::Error as JsonError;
use serde_urlencoded::ser::Error as FormError; use serde_urlencoded::ser::Error as FormError;
use tokio_timer::Error as TimerError;
// re-export for convinience // re-export for convinience
#[cfg(feature = "cookies")]
pub use cookie::ParseError as CookieParseError;
use crate::body::Body; use crate::body::Body;
use crate::response::Response; pub use crate::cookie::ParseError as CookieParseError;
use crate::helpers::Writer;
use crate::response::{Response, ResponseBuilder};
/// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) /// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html)
/// for actix web operations /// for actix web operations
@ -45,42 +45,97 @@ pub type Result<T, E = Error> = result::Result<T, E>;
/// if you have access to an actix `Error` you can always get a /// if you have access to an actix `Error` you can always get a
/// `ResponseError` reference from it. /// `ResponseError` reference from it.
pub struct Error { pub struct Error {
cause: Box<ResponseError>, cause: Box<dyn ResponseError>,
} }
impl Error { impl Error {
/// Returns the reference to the underlying `ResponseError`. /// Returns the reference to the underlying `ResponseError`.
pub fn as_response_error(&self) -> &ResponseError { pub fn as_response_error(&self) -> &dyn ResponseError {
self.cause.as_ref() self.cause.as_ref()
} }
/// Converts error to a response instance and set error message as response body /// Similar to `as_response_error` but downcasts.
pub fn response_with_message(self) -> Response { pub fn as_error<T: ResponseError + 'static>(&self) -> Option<&T> {
let message = format!("{}", self); ResponseError::downcast_ref(self.cause.as_ref())
let resp: Response = self.into();
resp.set_body(Body::from(message))
} }
} }
/// Error that can be converted to `Response` /// Error that can be converted to `Response`
pub trait ResponseError: fmt::Debug + fmt::Display { pub trait ResponseError: fmt::Debug + fmt::Display {
/// Response's status code
///
/// Internal server error is generated by default.
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
/// Create response for error /// Create response for error
/// ///
/// Internal server error is generated by default. /// Internal server error is generated by default.
fn error_response(&self) -> Response { fn error_response(&self) -> Response {
Response::new(StatusCode::INTERNAL_SERVER_ERROR) let mut resp = Response::new(self.status_code());
let mut buf = BytesMut::new();
let _ = write!(Writer(&mut buf), "{}", self);
resp.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain; charset=utf-8"),
);
resp.set_body(Body::from(buf))
}
#[doc(hidden)]
fn __private_get_type_id__(&self) -> TypeId
where
Self: 'static,
{
TypeId::of::<Self>()
}
}
impl dyn ResponseError + 'static {
/// Downcasts a response error to a specific type.
pub fn downcast_ref<T: ResponseError + 'static>(&self) -> Option<&T> {
if self.__private_get_type_id__() == TypeId::of::<T>() {
unsafe { Some(&*(self as *const dyn ResponseError as *const T)) }
} else {
None
}
} }
} }
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.cause, f) fmt::Display::fmt(&self.cause, f)
} }
} }
impl fmt::Debug for Error { impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}\n", &self.cause) write!(f, "{:?}", &self.cause)
}
}
impl std::error::Error for Error {
fn cause(&self) -> Option<&dyn std::error::Error> {
None
}
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl From<()> for Error {
fn from(_: ()) -> Self {
Error::from(UnitError)
}
}
impl From<std::convert::Infallible> for Error {
fn from(_: std::convert::Infallible) -> Self {
// `std::convert::Infallible` indicates an error
// that will never happen
unreachable!()
} }
} }
@ -100,39 +155,68 @@ impl<T: ResponseError + 'static> From<T> for Error {
} }
} }
/// Convert Response to a Error
impl From<Response> for Error {
fn from(res: Response) -> Error {
InternalError::from_response("", res).into()
}
}
/// Convert ResponseBuilder to a Error
impl From<ResponseBuilder> for Error {
fn from(mut res: ResponseBuilder) -> Error {
InternalError::from_response("", res.finish()).into()
}
}
/// Return `GATEWAY_TIMEOUT` for `TimeoutError` /// Return `GATEWAY_TIMEOUT` for `TimeoutError`
impl<E: ResponseError> ResponseError for TimeoutError<E> { impl<E: ResponseError> ResponseError for TimeoutError<E> {
fn error_response(&self) -> Response { fn status_code(&self) -> StatusCode {
match self { match self {
TimeoutError::Service(e) => e.error_response(), TimeoutError::Service(e) => e.status_code(),
TimeoutError::Timeout => Response::new(StatusCode::GATEWAY_TIMEOUT), TimeoutError::Timeout => StatusCode::GATEWAY_TIMEOUT,
} }
} }
} }
#[derive(Debug, Display)]
#[display(fmt = "UnknownError")]
struct UnitError;
/// `InternalServerError` for `UnitError`
impl ResponseError for UnitError {}
/// `InternalServerError` for `JsonError` /// `InternalServerError` for `JsonError`
impl ResponseError for JsonError {} impl ResponseError for JsonError {}
/// `InternalServerError` for `FormError` /// `InternalServerError` for `FormError`
impl ResponseError for FormError {} impl ResponseError for FormError {}
/// `InternalServerError` for `TimerError` #[cfg(feature = "openssl")]
impl ResponseError for TimerError {} /// `InternalServerError` for `openssl::ssl::Error`
impl ResponseError for actix_connect::ssl::openssl::SslError {}
#[cfg(feature = "openssl")]
/// `InternalServerError` for `openssl::ssl::HandshakeError`
impl<T: std::fmt::Debug> ResponseError for actix_tls::openssl::HandshakeError<T> {}
/// Return `BAD_REQUEST` for `de::value::Error` /// Return `BAD_REQUEST` for `de::value::Error`
impl ResponseError for DeError { impl ResponseError for DeError {
fn error_response(&self) -> Response { fn status_code(&self) -> StatusCode {
Response::new(StatusCode::BAD_REQUEST) StatusCode::BAD_REQUEST
} }
} }
/// `InternalServerError` for `Canceled`
impl ResponseError for Canceled {}
/// `InternalServerError` for `BlockingError` /// `InternalServerError` for `BlockingError`
impl<E: fmt::Debug> ResponseError for BlockingError<E> {} impl<E: fmt::Debug> ResponseError for BlockingError<E> {}
/// Return `BAD_REQUEST` for `Utf8Error` /// Return `BAD_REQUEST` for `Utf8Error`
impl ResponseError for Utf8Error { impl ResponseError for Utf8Error {
fn error_response(&self) -> Response { fn status_code(&self) -> StatusCode {
Response::new(StatusCode::BAD_REQUEST) StatusCode::BAD_REQUEST
} }
} }
@ -142,35 +226,22 @@ impl ResponseError for HttpError {}
/// Return `InternalServerError` for `io::Error` /// Return `InternalServerError` for `io::Error`
impl ResponseError for io::Error { impl ResponseError for io::Error {
fn error_response(&self) -> Response { fn status_code(&self) -> StatusCode {
match self.kind() { match self.kind() {
io::ErrorKind::NotFound => Response::new(StatusCode::NOT_FOUND), io::ErrorKind::NotFound => StatusCode::NOT_FOUND,
io::ErrorKind::PermissionDenied => Response::new(StatusCode::FORBIDDEN), io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN,
_ => Response::new(StatusCode::INTERNAL_SERVER_ERROR), _ => StatusCode::INTERNAL_SERVER_ERROR,
} }
} }
} }
/// `BadRequest` for `InvalidHeaderValue` /// `BadRequest` for `InvalidHeaderValue`
impl ResponseError for header::InvalidHeaderValue { impl ResponseError for header::InvalidHeaderValue {
fn error_response(&self) -> Response { fn status_code(&self) -> StatusCode {
Response::new(StatusCode::BAD_REQUEST) StatusCode::BAD_REQUEST
} }
} }
/// `BadRequest` for `InvalidHeaderValue`
impl ResponseError for header::InvalidHeaderValueBytes {
fn error_response(&self) -> Response {
Response::new(StatusCode::BAD_REQUEST)
}
}
/// `InternalServerError` for `futures::Canceled`
impl ResponseError for Canceled {}
// /// `InternalServerError` for `actix::MailboxError`
// impl ResponseError for MailboxError {}
/// A set of errors that can occur during parsing HTTP streams /// A set of errors that can occur during parsing HTTP streams
#[derive(Debug, Display)] #[derive(Debug, Display)]
pub enum ParseError { pub enum ParseError {
@ -210,8 +281,8 @@ pub enum ParseError {
/// Return `BadRequest` for `ParseError` /// Return `BadRequest` for `ParseError`
impl ResponseError for ParseError { impl ResponseError for ParseError {
fn error_response(&self) -> Response { fn status_code(&self) -> StatusCode {
Response::new(StatusCode::BAD_REQUEST) StatusCode::BAD_REQUEST
} }
} }
@ -303,7 +374,7 @@ impl From<BlockingError<io::Error>> for PayloadError {
BlockingError::Error(e) => PayloadError::Io(e), BlockingError::Error(e) => PayloadError::Io(e),
BlockingError::Canceled => PayloadError::Io(io::Error::new( BlockingError::Canceled => PayloadError::Io(io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
"Thread pool is gone", "Operation is canceled",
)), )),
} }
} }
@ -314,19 +385,18 @@ impl From<BlockingError<io::Error>> for PayloadError {
/// - `Overflow` returns `PayloadTooLarge` /// - `Overflow` returns `PayloadTooLarge`
/// - Other errors returns `BadRequest` /// - Other errors returns `BadRequest`
impl ResponseError for PayloadError { impl ResponseError for PayloadError {
fn error_response(&self) -> Response { fn status_code(&self) -> StatusCode {
match *self { match *self {
PayloadError::Overflow => Response::new(StatusCode::PAYLOAD_TOO_LARGE), PayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE,
_ => Response::new(StatusCode::BAD_REQUEST), _ => StatusCode::BAD_REQUEST,
} }
} }
} }
/// Return `BadRequest` for `cookie::ParseError` /// Return `BadRequest` for `cookie::ParseError`
#[cfg(feature = "cookies")] impl ResponseError for crate::cookie::ParseError {
impl ResponseError for cookie::ParseError { fn status_code(&self) -> StatusCode {
fn error_response(&self) -> Response { StatusCode::BAD_REQUEST
Response::new(StatusCode::BAD_REQUEST)
} }
} }
@ -334,7 +404,10 @@ impl ResponseError for cookie::ParseError {
/// A set of errors that can occur during dispatching http requests /// A set of errors that can occur during dispatching http requests
pub enum DispatchError { pub enum DispatchError {
/// Service error /// Service error
Service, Service(Error),
/// Upgrade service error
Upgrade,
/// An `io::Error` that occurred while trying to read or write to a network /// An `io::Error` that occurred while trying to read or write to a network
/// stream. /// stream.
@ -387,11 +460,19 @@ pub enum ContentTypeError {
/// Return `BadRequest` for `ContentTypeError` /// Return `BadRequest` for `ContentTypeError`
impl ResponseError for ContentTypeError { impl ResponseError for ContentTypeError {
fn error_response(&self) -> Response { fn status_code(&self) -> StatusCode {
Response::new(StatusCode::BAD_REQUEST) StatusCode::BAD_REQUEST
} }
} }
impl<E, U: Encoder + Decoder> ResponseError for FramedDispatcherError<E, U>
where
E: fmt::Debug + fmt::Display,
<U as Encoder>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
}
/// Helper type that can wrap any error and generate custom response. /// Helper type that can wrap any error and generate custom response.
/// ///
/// In following example any `io::Error` will be converted into "BAD REQUEST" /// In following example any `io::Error` will be converted into "BAD REQUEST"
@ -399,14 +480,12 @@ impl ResponseError for ContentTypeError {
/// default. /// default.
/// ///
/// ```rust /// ```rust
/// # extern crate actix_http;
/// # use std::io; /// # use std::io;
/// # use actix_http::*; /// # use actix_http::*;
/// ///
/// fn index(req: Request) -> Result<&'static str> { /// fn index(req: Request) -> Result<&'static str> {
/// Err(error::ErrorBadRequest(io::Error::new(io::ErrorKind::Other, "error"))) /// Err(error::ErrorBadRequest(io::Error::new(io::ErrorKind::Other, "error")))
/// } /// }
/// # fn main() {}
/// ``` /// ```
pub struct InternalError<T> { pub struct InternalError<T> {
cause: T, cause: T,
@ -440,7 +519,7 @@ impl<T> fmt::Debug for InternalError<T>
where where
T: fmt::Debug + 'static, T: fmt::Debug + 'static,
{ {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.cause, f) fmt::Debug::fmt(&self.cause, f)
} }
} }
@ -449,7 +528,7 @@ impl<T> fmt::Display for InternalError<T>
where where
T: fmt::Display + 'static, T: fmt::Display + 'static,
{ {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.cause, f) fmt::Display::fmt(&self.cause, f)
} }
} }
@ -458,9 +537,31 @@ impl<T> ResponseError for InternalError<T>
where where
T: fmt::Debug + fmt::Display + 'static, T: fmt::Debug + fmt::Display + 'static,
{ {
fn status_code(&self) -> StatusCode {
match self.status {
InternalErrorType::Status(st) => st,
InternalErrorType::Response(ref resp) => {
if let Some(resp) = resp.borrow().as_ref() {
resp.head().status
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}
fn error_response(&self) -> Response { fn error_response(&self) -> Response {
match self.status { match self.status {
InternalErrorType::Status(st) => Response::new(st), InternalErrorType::Status(st) => {
let mut res = Response::new(st);
let mut buf = BytesMut::new();
let _ = write!(Writer(&mut buf), "{}", self);
res.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain; charset=utf-8"),
);
res.set_body(Body::from(buf))
}
InternalErrorType::Response(ref resp) => { InternalErrorType::Response(ref resp) => {
if let Some(resp) = resp.borrow_mut().take() { if let Some(resp) = resp.borrow_mut().take() {
resp resp
@ -472,13 +573,6 @@ where
} }
} }
/// Convert Response to a Error
impl From<Response> for Error {
fn from(res: Response) -> Error {
InternalError::from_response("", res).into()
}
}
/// Helper function that creates wrapper of any error and generate *BAD /// Helper function that creates wrapper of any error and generate *BAD
/// REQUEST* response. /// REQUEST* response.
#[allow(non_snake_case)] #[allow(non_snake_case)]
@ -869,24 +963,15 @@ where
InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into() InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into()
} }
#[cfg(feature = "fail")] #[cfg(feature = "failure")]
mod failure_integration { /// Compatibility for `failure::Error`
use super::*; impl ResponseError for fail_ure::Error {}
/// Compatibility for `failure::Error`
impl ResponseError for failure::Error {
fn error_response(&self) -> Response {
Response::new(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use http::{Error as HttpError, StatusCode}; use http::{Error as HttpError, StatusCode};
use httparse; use httparse;
use std::error::Error as StdError;
use std::io; use std::io;
#[test] #[test]
@ -899,10 +984,8 @@ mod tests {
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }
#[cfg(feature = "cookies")]
#[test] #[test]
fn test_cookie_parse() { fn test_cookie_parse() {
use cookie::ParseError as CookieParseError;
let resp: Response = CookieParseError::EmptyName.error_response(); let resp: Response = CookieParseError::EmptyName.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
} }
@ -917,7 +1000,7 @@ mod tests {
#[test] #[test]
fn test_error_cause() { fn test_error_cause() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::new(io::ErrorKind::Other, "other");
let desc = orig.description().to_owned(); let desc = orig.to_string();
let e = Error::from(orig); let e = Error::from(orig);
assert_eq!(format!("{}", e.as_response_error()), desc); assert_eq!(format!("{}", e.as_response_error()), desc);
} }
@ -925,7 +1008,7 @@ mod tests {
#[test] #[test]
fn test_error_display() { fn test_error_display() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::new(io::ErrorKind::Other, "other");
let desc = orig.description().to_owned(); let desc = orig.to_string();
let e = Error::from(orig); let e = Error::from(orig);
assert_eq!(format!("{}", e), desc); assert_eq!(format!("{}", e), desc);
} }
@ -967,7 +1050,7 @@ mod tests {
match ParseError::from($from) { match ParseError::from($from) {
e @ $error => { e @ $error => {
let desc = format!("{}", e); let desc = format!("{}", e);
assert_eq!(desc, format!("IO error: {}", $from.description())); assert_eq!(desc, format!("IO error: {}", $from));
} }
_ => unreachable!("{:?}", $from), _ => unreachable!("{:?}", $from),
} }
@ -995,6 +1078,16 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[test]
fn test_error_casting() {
let err = PayloadError::Overflow;
let resp_err: &dyn ResponseError = &err;
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
assert_eq!(err.to_string(), "A payload reached size limit.");
let not_err = resp_err.downcast_ref::<ContentTypeError>();
assert!(not_err.is_none());
}
#[test] #[test]
fn test_error_helpers() { fn test_error_helpers() {
let r: Response = ErrorBadRequest("err").into(); let r: Response = ErrorBadRequest("err").into();

View File

@ -1,12 +1,12 @@
use std::any::{Any, TypeId}; use std::any::{Any, TypeId};
use std::fmt; use std::fmt;
use hashbrown::HashMap; use fxhash::FxHashMap;
#[derive(Default)] #[derive(Default)]
/// A type map of request extensions. /// A type map of request extensions.
pub struct Extensions { pub struct Extensions {
map: HashMap<TypeId, Box<Any>>, map: FxHashMap<TypeId, Box<dyn Any>>,
} }
impl Extensions { impl Extensions {
@ -14,7 +14,7 @@ impl Extensions {
#[inline] #[inline]
pub fn new() -> Extensions { pub fn new() -> Extensions {
Extensions { Extensions {
map: HashMap::default(), map: FxHashMap::default(),
} }
} }
@ -35,14 +35,14 @@ impl Extensions {
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 as &(Any + 'static)).downcast_ref()) .and_then(|boxed| (&**boxed as &(dyn Any + 'static)).downcast_ref())
} }
/// Get a mutable reference to a type previously inserted on this `Extensions`. /// Get a mutable reference to a type previously inserted on this `Extensions`.
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| (&mut **boxed as &mut (Any + 'static)).downcast_mut()) .and_then(|boxed| (&mut **boxed as &mut (dyn Any + 'static)).downcast_mut())
} }
/// Remove a type from this `Extensions`. /// Remove a type from this `Extensions`.
@ -50,7 +50,7 @@ impl Extensions {
/// If a extension of this type existed, it will be returned. /// If a extension of this type existed, it will be returned.
pub fn remove<T: 'static>(&mut self) -> Option<T> { pub fn remove<T: 'static>(&mut self) -> Option<T> {
self.map.remove(&TypeId::of::<T>()).and_then(|boxed| { self.map.remove(&TypeId::of::<T>()).and_then(|boxed| {
(boxed as Box<Any + 'static>) (boxed as Box<dyn Any + 'static>)
.downcast() .downcast()
.ok() .ok()
.map(|boxed| *boxed) .map(|boxed| *boxed)
@ -65,7 +65,7 @@ impl Extensions {
} }
impl fmt::Debug for Extensions { impl fmt::Debug for Extensions {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Extensions").finish() f.debug_struct("Extensions").finish()
} }
} }

View File

@ -1,22 +1,17 @@
#![allow(unused_imports, unused_variables, dead_code)] use std::io;
use std::io::{self, Write};
use actix_codec::{Decoder, Encoder}; use actix_codec::{Decoder, Encoder};
use bitflags::bitflags; use bitflags::bitflags;
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::header::{
HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE,
};
use http::{Method, Version}; use http::{Method, Version};
use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType};
use super::{decoder, encoder}; use super::{decoder, encoder, reserve_readbuf};
use super::{Message, MessageType}; use super::{Message, MessageType};
use crate::body::BodySize; use crate::body::BodySize;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::error::{ParseError, PayloadError}; use crate::error::{ParseError, PayloadError};
use crate::helpers; use crate::message::{ConnectionType, RequestHeadType, ResponseHead};
use crate::message::{ConnectionType, Head, MessagePool, RequestHead, ResponseHead};
bitflags! { bitflags! {
struct Flags: u8 { struct Flags: u8 {
@ -26,8 +21,6 @@ bitflags! {
} }
} }
const AVERAGE_HEADER_SIZE: usize = 30;
/// HTTP/1 Codec /// HTTP/1 Codec
pub struct ClientCodec { pub struct ClientCodec {
inner: ClientCodecInner, inner: ClientCodecInner,
@ -47,8 +40,7 @@ struct ClientCodecInner {
// encoder part // encoder part
flags: Flags, flags: Flags,
headers_size: u32, encoder: encoder::MessageEncoder<RequestHeadType>,
encoder: encoder::MessageEncoder<RequestHead>,
} }
impl Default for ClientCodec { impl Default for ClientCodec {
@ -76,7 +68,6 @@ impl ClientCodec {
ctype: ConnectionType::Close, ctype: ConnectionType::Close,
flags, flags,
headers_size: 0,
encoder: encoder::MessageEncoder::default(), encoder: encoder::MessageEncoder::default(),
}, },
} }
@ -150,6 +141,7 @@ impl Decoder for ClientCodec {
} else { } else {
self.inner.payload = None; self.inner.payload = None;
} }
reserve_readbuf(src);
Ok(Some(req)) Ok(Some(req))
} else { } else {
Ok(None) Ok(None)
@ -168,7 +160,10 @@ impl Decoder for ClientPayloadCodec {
); );
Ok(match self.inner.payload.as_mut().unwrap().decode(src)? { Ok(match self.inner.payload.as_mut().unwrap().decode(src)? {
Some(PayloadItem::Chunk(chunk)) => Some(Some(chunk)), Some(PayloadItem::Chunk(chunk)) => {
reserve_readbuf(src);
Some(Some(chunk))
}
Some(PayloadItem::Eof) => { Some(PayloadItem::Eof) => {
self.inner.payload.take(); self.inner.payload.take();
Some(None) Some(None)
@ -179,7 +174,7 @@ impl Decoder for ClientPayloadCodec {
} }
impl Encoder for ClientCodec { impl Encoder for ClientCodec {
type Item = Message<(RequestHead, BodySize)>; type Item = Message<(RequestHeadType, BodySize)>;
type Error = io::Error; type Error = io::Error;
fn encode( fn encode(
@ -188,13 +183,15 @@ impl Encoder for ClientCodec {
dst: &mut BytesMut, dst: &mut BytesMut,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
match item { match item {
Message::Item((mut msg, length)) => { Message::Item((mut head, length)) => {
let inner = &mut self.inner; let inner = &mut self.inner;
inner.version = msg.version; inner.version = head.as_ref().version;
inner.flags.set(Flags::HEAD, msg.method == Method::HEAD); inner
.flags
.set(Flags::HEAD, head.as_ref().method == Method::HEAD);
// connection status // connection status
inner.ctype = match msg.connection_type() { inner.ctype = match head.as_ref().connection_type() {
ConnectionType::KeepAlive => { ConnectionType::KeepAlive => {
if inner.flags.contains(Flags::KEEPALIVE_ENABLED) { if inner.flags.contains(Flags::KEEPALIVE_ENABLED) {
ConnectionType::KeepAlive ConnectionType::KeepAlive
@ -208,7 +205,7 @@ impl Encoder for ClientCodec {
inner.encoder.encode( inner.encoder.encode(
dst, dst,
&mut msg, &mut head,
false, false,
false, false,
inner.version, inner.version,

View File

@ -1,12 +1,9 @@
#![allow(unused_imports, unused_variables, dead_code)] use std::{fmt, io};
use std::fmt;
use std::io::{self, Write};
use actix_codec::{Decoder, Encoder}; use actix_codec::{Decoder, Encoder};
use bitflags::bitflags; use bitflags::bitflags;
use bytes::{BufMut, Bytes, BytesMut}; use bytes::BytesMut;
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::{Method, Version};
use http::{Method, StatusCode, Version};
use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType};
use super::{decoder, encoder}; use super::{decoder, encoder};
@ -14,21 +11,18 @@ use super::{Message, MessageType};
use crate::body::BodySize; use crate::body::BodySize;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::error::ParseError; use crate::error::ParseError;
use crate::helpers; use crate::message::ConnectionType;
use crate::message::{ConnectionType, Head, ResponseHead};
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
bitflags! { bitflags! {
struct Flags: u8 { struct Flags: u8 {
const HEAD = 0b0000_0001; const HEAD = 0b0000_0001;
const KEEPALIVE_ENABLED = 0b0000_1000; const KEEPALIVE_ENABLED = 0b0000_0010;
const STREAM = 0b0001_0000; const STREAM = 0b0000_0100;
} }
} }
const AVERAGE_HEADER_SIZE: usize = 30;
/// HTTP/1 Codec /// HTTP/1 Codec
pub struct Codec { pub struct Codec {
config: ServiceConfig, config: ServiceConfig,
@ -39,7 +33,6 @@ pub struct Codec {
// encoder part // encoder part
flags: Flags, flags: Flags,
headers_size: u32,
encoder: encoder::MessageEncoder<Response<()>>, encoder: encoder::MessageEncoder<Response<()>>,
} }
@ -50,7 +43,7 @@ impl Default for Codec {
} }
impl fmt::Debug for Codec { impl fmt::Debug for Codec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "h1::Codec({:?})", self.flags) write!(f, "h1::Codec({:?})", self.flags)
} }
} }
@ -67,27 +60,34 @@ impl Codec {
}; };
Codec { Codec {
config, config,
flags,
decoder: decoder::MessageDecoder::default(), decoder: decoder::MessageDecoder::default(),
payload: None, payload: None,
version: Version::HTTP_11, version: Version::HTTP_11,
ctype: ConnectionType::Close, ctype: ConnectionType::Close,
flags,
headers_size: 0,
encoder: encoder::MessageEncoder::default(), encoder: encoder::MessageEncoder::default(),
} }
} }
#[inline]
/// Check if request is upgrade /// Check if request is upgrade
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.ctype == ConnectionType::Upgrade self.ctype == ConnectionType::Upgrade
} }
#[inline]
/// Check if last response is keep-alive /// Check if last response is keep-alive
pub fn keepalive(&self) -> bool { pub fn keepalive(&self) -> bool {
self.ctype == ConnectionType::KeepAlive self.ctype == ConnectionType::KeepAlive
} }
#[inline]
/// Check if keep-alive enabled on server level
pub fn keepalive_enabled(&self) -> bool {
self.flags.contains(Flags::KEEPALIVE_ENABLED)
}
#[inline]
/// Check last request's message type /// Check last request's message type
pub fn message_type(&self) -> MessageType { pub fn message_type(&self) -> MessageType {
if self.flags.contains(Flags::STREAM) { if self.flags.contains(Flags::STREAM) {
@ -98,6 +98,11 @@ impl Codec {
MessageType::Payload MessageType::Payload
} }
} }
#[inline]
pub fn config(&self) -> &ServiceConfig {
&self.config
}
} }
impl Decoder for Codec { impl Decoder for Codec {
@ -165,7 +170,6 @@ impl Encoder for Codec {
}; };
// encode message // encode message
let len = dst.len();
self.encoder.encode( self.encoder.encode(
dst, dst,
&mut res, &mut res,
@ -176,7 +180,7 @@ impl Encoder for Codec {
self.ctype, self.ctype,
&self.config, &self.config,
)?; )?;
self.headers_size = (dst.len() - len) as u32; // self.headers_size = (dst.len() - len) as u32;
} }
Message::Chunk(Some(bytes)) => { Message::Chunk(Some(bytes)) => {
self.encoder.encode_chunk(bytes.as_ref(), dst)?; self.encoder.encode_chunk(bytes.as_ref(), dst)?;
@ -191,17 +195,11 @@ impl Encoder for Codec {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{cmp, io}; use bytes::BytesMut;
use http::Method;
use actix_codec::{AsyncRead, AsyncWrite};
use bytes::{Buf, Bytes, BytesMut};
use http::{Method, Version};
use super::*; use super::*;
use crate::error::ParseError;
use crate::h1::Message;
use crate::httpmessage::HttpMessage; use crate::httpmessage::HttpMessage;
use crate::request::Request;
#[test] #[test]
fn test_http_request_chunked_payload_and_next_message() { fn test_http_request_chunked_payload_and_next_message() {

View File

@ -1,15 +1,18 @@
use std::convert::TryFrom;
use std::io;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::{io, mem}; use std::mem::MaybeUninit;
use std::task::Poll;
use actix_codec::Decoder; use actix_codec::Decoder;
use bytes::{Bytes, BytesMut}; use bytes::{Buf, Bytes, BytesMut};
use futures::{Async, Poll};
use http::header::{HeaderName, HeaderValue}; use http::header::{HeaderName, HeaderValue};
use http::{header, HeaderMap, HttpTryFrom, Method, StatusCode, Uri, Version}; use http::{header, Method, StatusCode, Uri, Version};
use httparse; use httparse;
use log::{debug, error, trace}; use log::{debug, error, trace};
use crate::error::ParseError; use crate::error::ParseError;
use crate::header::HeaderMap;
use crate::message::{ConnectionType, ResponseHead}; use crate::message::{ConnectionType, ResponseHead};
use crate::request::Request; use crate::request::Request;
@ -51,6 +54,8 @@ pub(crate) enum PayloadLength {
pub(crate) trait MessageType: Sized { pub(crate) trait MessageType: Sized {
fn set_connection_type(&mut self, ctype: Option<ConnectionType>); fn set_connection_type(&mut self, ctype: Option<ConnectionType>);
fn set_expect(&mut self);
fn headers_mut(&mut self) -> &mut HeaderMap; fn headers_mut(&mut self) -> &mut HeaderMap;
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError>; fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError>;
@ -62,6 +67,7 @@ pub(crate) trait MessageType: Sized {
) -> Result<PayloadLength, ParseError> { ) -> Result<PayloadLength, ParseError> {
let mut ka = None; let mut ka = None;
let mut has_upgrade = false; let mut has_upgrade = false;
let mut expect = false;
let mut chunked = false; let mut chunked = false;
let mut content_length = None; let mut content_length = None;
@ -69,73 +75,81 @@ pub(crate) trait MessageType: Sized {
let headers = self.headers_mut(); let headers = self.headers_mut();
for idx in raw_headers.iter() { for idx in raw_headers.iter() {
if let Ok(name) = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]) let name =
{ HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap();
// Unsafe: httparse check header value for valid utf-8
let value = unsafe { // Unsafe: httparse check header value for valid utf-8
HeaderValue::from_shared_unchecked( let value = unsafe {
slice.slice(idx.value.0, idx.value.1), HeaderValue::from_maybe_shared_unchecked(
) slice.slice(idx.value.0..idx.value.1),
}; )
match name { };
header::CONTENT_LENGTH => { match name {
if let Ok(s) = value.to_str() { header::CONTENT_LENGTH => {
if let Ok(len) = s.parse::<u64>() { if let Ok(s) = value.to_str() {
if let Ok(len) = s.parse::<u64>() {
if len != 0 {
content_length = Some(len); content_length = Some(len);
} else {
debug!("illegal Content-Length: {:?}", s);
return Err(ParseError::Header);
} }
} else { } else {
debug!("illegal Content-Length: {:?}", value); debug!("illegal Content-Length: {:?}", s);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} else {
debug!("illegal Content-Length: {:?}", value);
return Err(ParseError::Header);
} }
// transfer-encoding }
header::TRANSFER_ENCODING => { // transfer-encoding
if let Ok(s) = value.to_str().map(|s| s.trim()) { header::TRANSFER_ENCODING => {
chunked = s.eq_ignore_ascii_case("chunked"); if let Ok(s) = value.to_str().map(|s| s.trim()) {
} else { chunked = s.eq_ignore_ascii_case("chunked");
return Err(ParseError::Header); } else {
} return Err(ParseError::Header);
} }
// connection keep-alive state }
header::CONNECTION => { // connection keep-alive state
ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) header::CONNECTION => {
{ ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) {
if conn.eq_ignore_ascii_case("keep-alive") { if conn.eq_ignore_ascii_case("keep-alive") {
Some(ConnectionType::KeepAlive) Some(ConnectionType::KeepAlive)
} else if conn.eq_ignore_ascii_case("close") { } else if conn.eq_ignore_ascii_case("close") {
Some(ConnectionType::Close) Some(ConnectionType::Close)
} else if conn.eq_ignore_ascii_case("upgrade") { } else if conn.eq_ignore_ascii_case("upgrade") {
Some(ConnectionType::Upgrade) Some(ConnectionType::Upgrade)
} else {
None
}
} else { } else {
None None
}; }
} } else {
header::UPGRADE => { None
has_upgrade = true; };
// check content-length, some clients (dart) }
// sends "content-length: 0" with websocket upgrade header::UPGRADE => {
if let Ok(val) = value.to_str().map(|val| val.trim()) { has_upgrade = true;
if val.eq_ignore_ascii_case("websocket") { // check content-length, some clients (dart)
content_length = None; // sends "content-length: 0" with websocket upgrade
} if let Ok(val) = value.to_str().map(|val| val.trim()) {
if val.eq_ignore_ascii_case("websocket") {
content_length = None;
} }
} }
_ => (),
} }
header::EXPECT => {
headers.append(name, value); let bytes = value.as_bytes();
} else { if bytes.len() >= 4 && &bytes[0..4] == b"100-" {
return Err(ParseError::Header); expect = true;
}
}
_ => (),
} }
headers.append(name, value);
} }
} }
self.set_connection_type(ka); self.set_connection_type(ka);
if expect {
self.set_expect()
}
// https://tools.ietf.org/html/rfc7230#section-3.3.3 // https://tools.ietf.org/html/rfc7230#section-3.3.3
if chunked { if chunked {
@ -163,18 +177,24 @@ impl MessageType for Request {
} }
} }
fn set_expect(&mut self) {
self.head_mut().set_expect();
}
fn headers_mut(&mut self) -> &mut HeaderMap { fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.head_mut().headers &mut self.head_mut().headers
} }
#[allow(clippy::uninit_assumed_init)]
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> { fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
// Unsafe: we read only this data only after httparse parses headers into. // Unsafe: we read only this data only after httparse parses headers into.
// performance bump for pipeline benchmarks. // performance bump for pipeline benchmarks.
let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; let mut headers: [HeaderIndex; MAX_HEADERS] =
unsafe { MaybeUninit::uninit().assume_init() };
let (len, method, uri, ver, h_len) = { let (len, method, uri, ver, h_len) = {
let mut parsed: [httparse::Header; MAX_HEADERS] = let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
unsafe { mem::uninitialized() }; unsafe { MaybeUninit::uninit().assume_init() };
let mut req = httparse::Request::new(&mut parsed); let mut req = httparse::Request::new(&mut parsed);
match req.parse(src)? { match req.parse(src)? {
@ -198,10 +218,10 @@ impl MessageType for Request {
let mut msg = Request::new(); let mut msg = Request::new();
// convert headers // convert headers
let len = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?;
// payload decoder // payload decoder
let decoder = match len { let decoder = match length {
PayloadLength::Payload(pl) => pl, PayloadLength::Payload(pl) => pl,
PayloadLength::Upgrade => { PayloadLength::Upgrade => {
// upgrade(websocket) // upgrade(websocket)
@ -235,18 +255,22 @@ impl MessageType for ResponseHead {
} }
} }
fn set_expect(&mut self) {}
fn headers_mut(&mut self) -> &mut HeaderMap { fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers &mut self.headers
} }
#[allow(clippy::uninit_assumed_init)]
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> { fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
// Unsafe: we read only this data only after httparse parses headers into. // Unsafe: we read only this data only after httparse parses headers into.
// performance bump for pipeline benchmarks. // performance bump for pipeline benchmarks.
let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; let mut headers: [HeaderIndex; MAX_HEADERS] =
unsafe { MaybeUninit::uninit().assume_init() };
let (len, ver, status, h_len) = { let (len, ver, status, h_len) = {
let mut parsed: [httparse::Header; MAX_HEADERS] = let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
unsafe { mem::uninitialized() }; unsafe { MaybeUninit::uninit().assume_init() };
let mut res = httparse::Response::new(&mut parsed); let mut res = httparse::Response::new(&mut parsed);
match res.parse(src)? { match res.parse(src)? {
@ -266,13 +290,14 @@ impl MessageType for ResponseHead {
} }
}; };
let mut msg = ResponseHead::default(); let mut msg = ResponseHead::new(status);
msg.version = ver;
// convert headers // convert headers
let len = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?;
// message payload // message payload
let decoder = if let PayloadLength::Payload(pl) = len { let decoder = if let PayloadLength::Payload(pl) = length {
pl pl
} else if status == StatusCode::SWITCHING_PROTOCOLS { } else if status == StatusCode::SWITCHING_PROTOCOLS {
// switching protocol or connect // switching protocol or connect
@ -281,12 +306,15 @@ impl MessageType for ResponseHead {
error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
return Err(ParseError::TooLarge); return Err(ParseError::TooLarge);
} else { } else {
PayloadType::None // for HTTP/1.0 read to eof and close connection
if msg.version == Version::HTTP_10 {
msg.set_connection_type(ConnectionType::Close);
PayloadType::Payload(PayloadDecoder::eof())
} else {
PayloadType::None
}
}; };
msg.status = status;
msg.version = ver;
Ok(Some((msg, decoder))) Ok(Some((msg, decoder)))
} }
} }
@ -300,7 +328,7 @@ pub(crate) struct HeaderIndex {
impl HeaderIndex { impl HeaderIndex {
pub(crate) fn record( pub(crate) fn record(
bytes: &[u8], bytes: &[u8],
headers: &[httparse::Header], headers: &[httparse::Header<'_>],
indices: &mut [HeaderIndex], indices: &mut [HeaderIndex],
) { ) {
let bytes_ptr = bytes.as_ptr() as usize; let bytes_ptr = bytes.as_ptr() as usize;
@ -315,7 +343,7 @@ impl HeaderIndex {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
/// Http payload item /// Http payload item
pub enum PayloadItem { pub enum PayloadItem {
Chunk(Bytes), Chunk(Bytes),
@ -403,7 +431,7 @@ impl Decoder for PayloadDecoder {
let len = src.len() as u64; let len = src.len() as u64;
let buf; let buf;
if *remaining > len { if *remaining > len {
buf = src.take().freeze(); buf = src.split().freeze();
*remaining -= len; *remaining -= len;
} else { } else {
buf = src.split_to(*remaining as usize).freeze(); buf = src.split_to(*remaining as usize).freeze();
@ -417,9 +445,10 @@ impl Decoder for PayloadDecoder {
loop { loop {
let mut buf = None; let mut buf = None;
// advances the chunked state // advances the chunked state
*state = match state.step(src, size, &mut buf)? { *state = match state.step(src, size, &mut buf) {
Async::NotReady => return Ok(None), Poll::Pending => return Ok(None),
Async::Ready(state) => state, Poll::Ready(Ok(state)) => state,
Poll::Ready(Err(e)) => return Err(e),
}; };
if *state == ChunkedState::End { if *state == ChunkedState::End {
trace!("End of chunked stream"); trace!("End of chunked stream");
@ -437,7 +466,7 @@ impl Decoder for PayloadDecoder {
if src.is_empty() { if src.is_empty() {
Ok(None) Ok(None)
} else { } else {
Ok(Some(PayloadItem::Chunk(src.take().freeze()))) Ok(Some(PayloadItem::Chunk(src.split().freeze())))
} }
} }
} }
@ -448,10 +477,10 @@ macro_rules! byte (
($rdr:ident) => ({ ($rdr:ident) => ({
if $rdr.len() > 0 { if $rdr.len() > 0 {
let b = $rdr[0]; let b = $rdr[0];
$rdr.split_to(1); $rdr.advance(1);
b b
} else { } else {
return Ok(Async::NotReady) return Poll::Pending
} }
}) })
); );
@ -462,7 +491,7 @@ impl ChunkedState {
body: &mut BytesMut, body: &mut BytesMut,
size: &mut u64, size: &mut u64,
buf: &mut Option<Bytes>, buf: &mut Option<Bytes>,
) -> Poll<ChunkedState, io::Error> { ) -> Poll<Result<ChunkedState, io::Error>> {
use self::ChunkedState::*; use self::ChunkedState::*;
match *self { match *self {
Size => ChunkedState::read_size(body, size), Size => ChunkedState::read_size(body, size),
@ -474,66 +503,71 @@ impl ChunkedState {
BodyLf => ChunkedState::read_body_lf(body), BodyLf => ChunkedState::read_body_lf(body),
EndCr => ChunkedState::read_end_cr(body), EndCr => ChunkedState::read_end_cr(body),
EndLf => ChunkedState::read_end_lf(body), EndLf => ChunkedState::read_end_lf(body),
End => Ok(Async::Ready(ChunkedState::End)), End => Poll::Ready(Ok(ChunkedState::End)),
} }
} }
fn read_size(rdr: &mut BytesMut, size: &mut u64) -> Poll<ChunkedState, io::Error> {
fn read_size(
rdr: &mut BytesMut,
size: &mut u64,
) -> Poll<Result<ChunkedState, io::Error>> {
let radix = 16; let radix = 16;
match byte!(rdr) { match byte!(rdr) {
b @ b'0'...b'9' => { b @ b'0'..=b'9' => {
*size *= radix; *size *= radix;
*size += u64::from(b - b'0'); *size += u64::from(b - b'0');
} }
b @ b'a'...b'f' => { b @ b'a'..=b'f' => {
*size *= radix; *size *= radix;
*size += u64::from(b + 10 - b'a'); *size += u64::from(b + 10 - b'a');
} }
b @ b'A'...b'F' => { b @ b'A'..=b'F' => {
*size *= radix; *size *= radix;
*size += u64::from(b + 10 - b'A'); *size += u64::from(b + 10 - b'A');
} }
b'\t' | b' ' => return Ok(Async::Ready(ChunkedState::SizeLws)), b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)),
b';' => return Ok(Async::Ready(ChunkedState::Extension)), b';' => return Poll::Ready(Ok(ChunkedState::Extension)),
b'\r' => return Ok(Async::Ready(ChunkedState::SizeLf)), b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)),
_ => { _ => {
return Err(io::Error::new( return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Invalid chunk size line: Invalid Size", "Invalid chunk size line: Invalid Size",
)); )));
} }
} }
Ok(Async::Ready(ChunkedState::Size)) Poll::Ready(Ok(ChunkedState::Size))
} }
fn read_size_lws(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> {
fn read_size_lws(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
trace!("read_size_lws"); trace!("read_size_lws");
match byte!(rdr) { match byte!(rdr) {
// LWS can follow the chunk size, but no more digits can come // LWS can follow the chunk size, but no more digits can come
b'\t' | b' ' => Ok(Async::Ready(ChunkedState::SizeLws)), b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)),
b';' => Ok(Async::Ready(ChunkedState::Extension)), b';' => Poll::Ready(Ok(ChunkedState::Extension)),
b'\r' => Ok(Async::Ready(ChunkedState::SizeLf)), b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
_ => Err(io::Error::new( _ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Invalid chunk size linear white space", "Invalid chunk size linear white space",
)), ))),
} }
} }
fn read_extension(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> { fn read_extension(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) { match byte!(rdr) {
b'\r' => Ok(Async::Ready(ChunkedState::SizeLf)), b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
_ => Ok(Async::Ready(ChunkedState::Extension)), // no supported extensions _ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
} }
} }
fn read_size_lf( fn read_size_lf(
rdr: &mut BytesMut, rdr: &mut BytesMut,
size: &mut u64, size: &mut u64,
) -> Poll<ChunkedState, io::Error> { ) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) { match byte!(rdr) {
b'\n' if *size > 0 => Ok(Async::Ready(ChunkedState::Body)), b'\n' if *size > 0 => Poll::Ready(Ok(ChunkedState::Body)),
b'\n' if *size == 0 => Ok(Async::Ready(ChunkedState::EndCr)), b'\n' if *size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)),
_ => Err(io::Error::new( _ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Invalid chunk size LF", "Invalid chunk size LF",
)), ))),
} }
} }
@ -541,16 +575,16 @@ impl ChunkedState {
rdr: &mut BytesMut, rdr: &mut BytesMut,
rem: &mut u64, rem: &mut u64,
buf: &mut Option<Bytes>, buf: &mut Option<Bytes>,
) -> Poll<ChunkedState, io::Error> { ) -> Poll<Result<ChunkedState, io::Error>> {
trace!("Chunked read, remaining={:?}", rem); trace!("Chunked read, remaining={:?}", rem);
let len = rdr.len() as u64; let len = rdr.len() as u64;
if len == 0 { if len == 0 {
Ok(Async::Ready(ChunkedState::Body)) Poll::Ready(Ok(ChunkedState::Body))
} else { } else {
let slice; let slice;
if *rem > len { if *rem > len {
slice = rdr.take().freeze(); slice = rdr.split().freeze();
*rem -= len; *rem -= len;
} else { } else {
slice = rdr.split_to(*rem as usize).freeze(); slice = rdr.split_to(*rem as usize).freeze();
@ -558,47 +592,47 @@ impl ChunkedState {
} }
*buf = Some(slice); *buf = Some(slice);
if *rem > 0 { if *rem > 0 {
Ok(Async::Ready(ChunkedState::Body)) Poll::Ready(Ok(ChunkedState::Body))
} else { } else {
Ok(Async::Ready(ChunkedState::BodyCr)) Poll::Ready(Ok(ChunkedState::BodyCr))
} }
} }
} }
fn read_body_cr(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> { fn read_body_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) { match byte!(rdr) {
b'\r' => Ok(Async::Ready(ChunkedState::BodyLf)), b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)),
_ => Err(io::Error::new( _ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Invalid chunk body CR", "Invalid chunk body CR",
)), ))),
} }
} }
fn read_body_lf(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> { fn read_body_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) { match byte!(rdr) {
b'\n' => Ok(Async::Ready(ChunkedState::Size)), b'\n' => Poll::Ready(Ok(ChunkedState::Size)),
_ => Err(io::Error::new( _ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Invalid chunk body LF", "Invalid chunk body LF",
)), ))),
} }
} }
fn read_end_cr(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> { fn read_end_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) { match byte!(rdr) {
b'\r' => Ok(Async::Ready(ChunkedState::EndLf)), b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)),
_ => Err(io::Error::new( _ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Invalid chunk end CR", "Invalid chunk end CR",
)), ))),
} }
} }
fn read_end_lf(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> { fn read_end_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) { match byte!(rdr) {
b'\n' => Ok(Async::Ready(ChunkedState::End)), b'\n' => Poll::Ready(Ok(ChunkedState::End)),
_ => Err(io::Error::new( _ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Invalid chunk end LF", "Invalid chunk end LF",
)), ))),
} }
} }
} }
@ -610,6 +644,7 @@ mod tests {
use super::*; use super::*;
use crate::error::ParseError; use crate::error::ParseError;
use crate::http::header::{HeaderName, SET_COOKIE};
use crate::httpmessage::HttpMessage; use crate::httpmessage::HttpMessage;
impl PayloadType { impl PayloadType {
@ -770,7 +805,13 @@ mod tests {
assert_eq!(req.version(), Version::HTTP_11); assert_eq!(req.version(), Version::HTTP_11);
assert_eq!(*req.method(), Method::GET); assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test"); assert_eq!(req.path(), "/test");
assert_eq!(req.headers().get("test").unwrap().as_bytes(), b"value"); assert_eq!(
req.headers()
.get(HeaderName::try_from("test").unwrap())
.unwrap()
.as_bytes(),
b"value"
);
} }
#[test] #[test]
@ -785,12 +826,11 @@ mod tests {
let val: Vec<_> = req let val: Vec<_> = req
.headers() .headers()
.get_all("Set-Cookie") .get_all(SET_COOKIE)
.iter()
.map(|v| v.to_str().unwrap().to_owned()) .map(|v| v.to_str().unwrap().to_owned())
.collect(); .collect();
assert_eq!(val[0], "c1=cookie1"); assert_eq!(val[1], "c1=cookie1");
assert_eq!(val[1], "c2=cookie2"); assert_eq!(val[0], "c2=cookie2");
} }
#[test] #[test]
@ -1169,4 +1209,16 @@ mod tests {
let msg = pl.decode(&mut buf).unwrap().unwrap(); let msg = pl.decode(&mut buf).unwrap().unwrap();
assert!(msg.eof()); assert!(msg.eof());
} }
#[test]
fn test_response_http10_read_until_eof() {
let mut buf = BytesMut::from(&"HTTP/1.0 200 Ok\r\n\r\ntest data"[..]);
let mut reader = MessageDecoder::<ResponseHead>::default();
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
let chunk = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"test data")));
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,18 @@
#![allow(unused_imports, unused_variables, dead_code)]
use std::fmt::Write as FmtWrite;
use std::io::Write; use std::io::Write;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::str::FromStr; use std::ptr::copy_nonoverlapping;
use std::{cmp, fmt, io, mem}; use std::slice::from_raw_parts_mut;
use std::{cmp, io};
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{buf::BufMutExt, BufMut, BytesMut};
use http::header::{
HeaderValue, ACCEPT_ENCODING, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING,
};
use http::{HeaderMap, Method, StatusCode, Version};
use crate::body::BodySize; use crate::body::BodySize;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::header::ContentEncoding; use crate::header::map;
use crate::helpers; use crate::helpers;
use crate::message::{ConnectionType, Head, RequestHead, ResponseHead}; use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use crate::request::Request; use crate::http::{HeaderMap, StatusCode, Version};
use crate::message::{ConnectionType, RequestHeadType};
use crate::response::Response; use crate::response::Response;
const AVERAGE_HEADER_SIZE: usize = 30; const AVERAGE_HEADER_SIZE: usize = 30;
@ -41,10 +37,14 @@ impl<T: MessageType> Default for MessageEncoder<T> {
pub(crate) trait MessageType: Sized { pub(crate) trait MessageType: Sized {
fn status(&self) -> Option<StatusCode>; fn status(&self) -> Option<StatusCode>;
// fn connection_type(&self) -> Option<ConnectionType>;
fn headers(&self) -> &HeaderMap; fn headers(&self) -> &HeaderMap;
fn extra_headers(&self) -> Option<&HeaderMap>;
fn camel_case(&self) -> bool {
false
}
fn chunked(&self) -> bool; fn chunked(&self) -> bool;
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()>; fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()>;
@ -59,6 +59,7 @@ pub(crate) trait MessageType: Sized {
) -> io::Result<()> { ) -> io::Result<()> {
let chunked = self.chunked(); let chunked = self.chunked();
let mut skip_len = length != BodySize::Stream; let mut skip_len = length != BodySize::Stream;
let camel_case = self.camel_case();
// Content length // Content length
if let Some(status) = self.status() { if let Some(status) = self.status() {
@ -76,42 +77,72 @@ pub(crate) trait MessageType: Sized {
match length { match length {
BodySize::Stream => { BodySize::Stream => {
if chunked { if chunked {
dst.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") if camel_case {
dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n")
} else {
dst.put_slice(b"\r\ntransfer-encoding: chunked\r\n")
}
} else { } else {
skip_len = false; skip_len = false;
dst.extend_from_slice(b"\r\n"); dst.put_slice(b"\r\n");
} }
} }
BodySize::Empty => { BodySize::Empty => {
dst.extend_from_slice(b"\r\ncontent-length: 0\r\n"); if camel_case {
dst.put_slice(b"\r\nContent-Length: 0\r\n");
} else {
dst.put_slice(b"\r\ncontent-length: 0\r\n");
}
} }
BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::Sized(len) => helpers::write_content_length(len, dst),
BodySize::Sized64(len) => { BodySize::Sized64(len) => {
dst.extend_from_slice(b"\r\ncontent-length: "); if camel_case {
write!(dst.writer(), "{}", len)?; dst.put_slice(b"\r\nContent-Length: ");
dst.extend_from_slice(b"\r\n"); } else {
dst.put_slice(b"\r\ncontent-length: ");
}
#[allow(clippy::write_with_newline)]
write!(dst.writer(), "{}\r\n", len)?;
} }
BodySize::None => dst.extend_from_slice(b"\r\n"), BodySize::None => dst.put_slice(b"\r\n"),
} }
// Connection // Connection
match ctype { match ctype {
ConnectionType::Upgrade => dst.extend_from_slice(b"connection: upgrade\r\n"), ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
ConnectionType::KeepAlive if version < Version::HTTP_11 => { ConnectionType::KeepAlive if version < Version::HTTP_11 => {
dst.extend_from_slice(b"connection: keep-alive\r\n") if camel_case {
dst.put_slice(b"Connection: keep-alive\r\n")
} else {
dst.put_slice(b"connection: keep-alive\r\n")
}
} }
ConnectionType::Close if version >= Version::HTTP_11 => { ConnectionType::Close if version >= Version::HTTP_11 => {
dst.extend_from_slice(b"connection: close\r\n") if camel_case {
dst.put_slice(b"Connection: close\r\n")
} else {
dst.put_slice(b"connection: close\r\n")
}
} }
_ => (), _ => (),
} }
// merging headers from head and extra headers. HeaderMap::new() does not allocate.
let empty_headers = HeaderMap::new();
let extra_headers = self.extra_headers().unwrap_or(&empty_headers);
let headers = self
.headers()
.inner
.iter()
.filter(|(name, _)| !extra_headers.contains_key(*name))
.chain(extra_headers.inner.iter());
// write headers // write headers
let mut pos = 0; let mut pos = 0;
let mut has_date = false; let mut has_date = false;
let mut remaining = dst.remaining_mut(); let mut remaining = dst.capacity() - dst.len();
let mut buf = unsafe { &mut *(dst.bytes_mut() as *mut [u8]) }; let mut buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
for (key, value) in self.headers() { for (key, value) in headers {
match *key { match *key {
CONNECTION => continue, CONNECTION => continue,
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue, TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue,
@ -120,31 +151,75 @@ pub(crate) trait MessageType: Sized {
} }
_ => (), _ => (),
} }
let v = value.as_ref();
let k = key.as_str().as_bytes(); let k = key.as_str().as_bytes();
let len = k.len() + v.len() + 4; match value {
if len > remaining { map::Value::One(ref val) => {
unsafe { let v = val.as_ref();
dst.advance_mut(pos); let v_len = v.len();
let k_len = k.len();
let len = k_len + v_len + 4;
if len > remaining {
unsafe {
dst.advance_mut(pos);
}
pos = 0;
dst.reserve(len * 2);
remaining = dst.capacity() - dst.len();
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
}
// use upper Camel-Case
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;
}
} }
pos = 0; map::Value::Multi(ref vec) => {
dst.reserve(len); for val in vec {
remaining = dst.remaining_mut(); let v = val.as_ref();
unsafe { let v_len = v.len();
buf = &mut *(dst.bytes_mut() as *mut _); let k_len = k.len();
let len = k_len + v_len + 4;
if len > remaining {
unsafe {
dst.advance_mut(pos);
}
pos = 0;
dst.reserve(len * 2);
remaining = dst.capacity() - dst.len();
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
}
// use upper Camel-Case
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[pos..pos + k.len()].copy_from_slice(k);
pos += k.len();
buf[pos..pos + 2].copy_from_slice(b": ");
pos += 2;
buf[pos..pos + v.len()].copy_from_slice(v);
pos += v.len();
buf[pos..pos + 2].copy_from_slice(b"\r\n");
pos += 2;
remaining -= len;
} }
unsafe { unsafe {
dst.advance_mut(pos); dst.advance_mut(pos);
@ -171,14 +246,14 @@ impl MessageType for Response<()> {
self.head().chunked() self.head().chunked()
} }
//fn connection_type(&self) -> Option<ConnectionType> {
// self.head().ctype
//}
fn headers(&self) -> &HeaderMap { fn headers(&self) -> &HeaderMap {
&self.head().headers &self.head().headers
} }
fn extra_headers(&self) -> Option<&HeaderMap> {
None
}
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> { fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> {
let head = self.head(); let head = self.head();
let reason = head.reason().as_bytes(); let reason = head.reason().as_bytes();
@ -186,35 +261,51 @@ impl MessageType for Response<()> {
// status line // status line
helpers::write_status_line(head.version, head.status.as_u16(), dst); helpers::write_status_line(head.version, head.status.as_u16(), dst);
dst.extend_from_slice(reason); dst.put_slice(reason);
Ok(()) Ok(())
} }
} }
impl MessageType for RequestHead { impl MessageType for RequestHeadType {
fn status(&self) -> Option<StatusCode> { fn status(&self) -> Option<StatusCode> {
None None
} }
fn chunked(&self) -> bool { fn chunked(&self) -> bool {
self.chunked() self.as_ref().chunked()
}
fn camel_case(&self) -> bool {
self.as_ref().camel_case_headers()
} }
fn headers(&self) -> &HeaderMap { fn headers(&self) -> &HeaderMap {
&self.headers self.as_ref().headers()
}
fn extra_headers(&self) -> Option<&HeaderMap> {
self.extra_headers()
} }
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> { fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> {
let head = self.as_ref();
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
write!( write!(
Writer(dst), Writer(dst),
"{} {} {}", "{} {} {}",
self.method, head.method,
self.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
match self.version { match head.version {
Version::HTTP_09 => "HTTP/0.9", Version::HTTP_09 => "HTTP/0.9",
Version::HTTP_10 => "HTTP/1.0", Version::HTTP_10 => "HTTP/1.0",
Version::HTTP_11 => "HTTP/1.1", Version::HTTP_11 => "HTTP/1.1",
Version::HTTP_2 => "HTTP/2.0", Version::HTTP_2 => "HTTP/2.0",
Version::HTTP_3 => "HTTP/3.0",
_ =>
return Err(io::Error::new(
io::ErrorKind::Other,
"unsupported version"
)),
} }
) )
.map_err(|e| io::Error::new(io::ErrorKind::Other, e)) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))
@ -396,10 +487,48 @@ impl<'a> io::Write for Writer<'a> {
} }
} }
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
copy_nonoverlapping(value.as_ptr(), buf, len);
}
fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
let mut index = 0;
let key = value;
let mut key_iter = key.iter();
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
} else {
return;
}
while let Some(c) = key_iter.next() {
buffer[index] = *c;
index += 1;
if *c == b'-' {
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
}
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use std::rc::Rc;
use bytes::Bytes; use bytes::Bytes;
use http::header::AUTHORIZATION;
use super::*;
use crate::http::header::{HeaderValue, CONTENT_TYPE};
use crate::RequestHead;
#[test] #[test]
fn test_chunked_te() { fn test_chunked_te() {
@ -410,8 +539,117 @@ mod tests {
assert!(enc.encode(b"", &mut bytes).ok().unwrap()); assert!(enc.encode(b"", &mut bytes).ok().unwrap());
} }
assert_eq!( assert_eq!(
bytes.take().freeze(), bytes.split().freeze(),
Bytes::from_static(b"4\r\ntest\r\n0\r\n\r\n") Bytes::from_static(b"4\r\ntest\r\n0\r\n\r\n")
); );
} }
#[test]
fn test_camel_case() {
let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default();
head.set_camel_case_headers(true);
head.headers.insert(DATE, HeaderValue::from_static("date"));
head.headers
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
let mut head = RequestHeadType::Owned(head);
let _ = head.encode_headers(
&mut bytes,
Version::HTTP_11,
BodySize::Empty,
ConnectionType::Close,
&ServiceConfig::default(),
);
let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
assert!(data.contains("Content-Length: 0\r\n"));
assert!(data.contains("Connection: close\r\n"));
assert!(data.contains("Content-Type: plain/text\r\n"));
assert!(data.contains("Date: date\r\n"));
let _ = head.encode_headers(
&mut bytes,
Version::HTTP_11,
BodySize::Stream,
ConnectionType::KeepAlive,
&ServiceConfig::default(),
);
let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
assert!(data.contains("Transfer-Encoding: chunked\r\n"));
assert!(data.contains("Content-Type: plain/text\r\n"));
assert!(data.contains("Date: date\r\n"));
let _ = head.encode_headers(
&mut bytes,
Version::HTTP_11,
BodySize::Sized64(100),
ConnectionType::KeepAlive,
&ServiceConfig::default(),
);
let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
assert!(data.contains("Content-Length: 100\r\n"));
assert!(data.contains("Content-Type: plain/text\r\n"));
assert!(data.contains("Date: date\r\n"));
let mut head = RequestHead::default();
head.set_camel_case_headers(false);
head.headers.insert(DATE, HeaderValue::from_static("date"));
head.headers
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
head.headers
.append(CONTENT_TYPE, HeaderValue::from_static("xml"));
let mut head = RequestHeadType::Owned(head);
let _ = head.encode_headers(
&mut bytes,
Version::HTTP_11,
BodySize::Stream,
ConnectionType::KeepAlive,
&ServiceConfig::default(),
);
let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
assert!(data.contains("transfer-encoding: chunked\r\n"));
assert!(data.contains("content-type: xml\r\n"));
assert!(data.contains("content-type: plain/text\r\n"));
assert!(data.contains("date: date\r\n"));
}
#[test]
fn test_extra_headers() {
let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default();
head.headers.insert(
AUTHORIZATION,
HeaderValue::from_static("some authorization"),
);
let mut extra_headers = HeaderMap::new();
extra_headers.insert(
AUTHORIZATION,
HeaderValue::from_static("another authorization"),
);
extra_headers.insert(DATE, HeaderValue::from_static("date"));
let mut head = RequestHeadType::Rc(Rc::new(head), Some(extra_headers));
let _ = head.encode_headers(
&mut bytes,
Version::HTTP_11,
BodySize::Empty,
ConnectionType::Close,
&ServiceConfig::default(),
);
let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
assert!(data.contains("content-length: 0\r\n"));
assert!(data.contains("connection: close\r\n"));
assert!(data.contains("authorization: another authorization\r\n"));
assert!(data.contains("date: date\r\n"));
}
} }

View File

@ -0,0 +1,38 @@
use std::task::{Context, Poll};
use actix_service::{Service, ServiceFactory};
use futures_util::future::{ok, Ready};
use crate::error::Error;
use crate::request::Request;
pub struct ExpectHandler;
impl ServiceFactory for ExpectHandler {
type Config = ();
type Request = Request;
type Response = Request;
type Error = Error;
type Service = ExpectHandler;
type InitError = Error;
type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
ok(ExpectHandler)
}
}
impl Service for ExpectHandler {
type Request = Request;
type Response = Request;
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request) -> Self::Future {
ok(req)
}
}

View File

@ -1,19 +1,25 @@
//! HTTP/1 implementation //! HTTP/1 implementation
use bytes::Bytes; use bytes::{Bytes, BytesMut};
mod client; mod client;
mod codec; mod codec;
mod decoder; mod decoder;
mod dispatcher; mod dispatcher;
mod encoder; mod encoder;
mod expect;
mod payload; mod payload;
mod service; mod service;
mod upgrade;
mod utils;
pub use self::client::{ClientCodec, ClientPayloadCodec}; pub use self::client::{ClientCodec, ClientPayloadCodec};
pub use self::codec::Codec; pub use self::codec::Codec;
pub use self::dispatcher::Dispatcher; pub use self::dispatcher::Dispatcher;
pub use self::payload::{Payload, PayloadBuffer}; pub use self::expect::ExpectHandler;
pub use self::payload::Payload;
pub use self::service::{H1Service, H1ServiceHandler, OneRequest}; pub use self::service::{H1Service, H1ServiceHandler, OneRequest};
pub use self::upgrade::UpgradeHandler;
pub use self::utils::SendResponse;
#[derive(Debug)] #[derive(Debug)]
/// Codec message /// Codec message
@ -38,6 +44,16 @@ pub enum MessageType {
Stream, Stream,
} }
const LW: usize = 2 * 1024;
const HW: usize = 32 * 1024;
pub(crate) fn reserve_readbuf(src: &mut BytesMut) {
let cap = src.capacity();
if cap < LW {
src.reserve(HW - cap);
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,13 +1,13 @@
//! Payload stream //! Payload stream
use std::cell::RefCell; use std::cell::RefCell;
use std::cmp;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::pin::Pin;
use std::rc::{Rc, Weak}; use std::rc::{Rc, Weak};
use std::task::{Context, Poll};
use bytes::{Bytes, BytesMut}; use actix_utils::task::LocalWaker;
use futures::task::current as current_task; use bytes::Bytes;
use futures::task::Task; use futures_core::Stream;
use futures::{Async, Poll, Stream};
use crate::error::PayloadError; use crate::error::PayloadError;
@ -15,7 +15,7 @@ use crate::error::PayloadError;
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768; pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub(crate) enum PayloadStatus { pub enum PayloadStatus {
Read, Read,
Pause, Pause,
Dropped, Dropped,
@ -80,88 +80,61 @@ impl Payload {
} }
#[inline] #[inline]
/// Set read buffer capacity pub fn readany(
/// &mut self,
/// Default buffer capacity is 32Kb. cx: &mut Context<'_>,
pub fn set_read_buffer_capacity(&mut self, cap: usize) { ) -> Poll<Option<Result<Bytes, PayloadError>>> {
self.inner.borrow_mut().capacity = cap; self.inner.borrow_mut().readany(cx)
} }
} }
impl Stream for Payload { impl Stream for Payload {
type Item = Bytes; type Item = Result<Bytes, PayloadError>;
type Error = PayloadError;
#[inline] fn poll_next(
fn poll(&mut self) -> Poll<Option<Bytes>, PayloadError> { self: Pin<&mut Self>,
self.inner.borrow_mut().readany() cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, PayloadError>>> {
self.inner.borrow_mut().readany(cx)
} }
} }
impl Clone for Payload {
fn clone(&self) -> Payload {
Payload {
inner: Rc::clone(&self.inner),
}
}
}
/// Payload writer interface.
pub(crate) trait PayloadWriter {
/// Set stream error.
fn set_error(&mut self, err: PayloadError);
/// Write eof into a stream which closes reading side of a stream.
fn feed_eof(&mut self);
/// Feed bytes into a payload stream
fn feed_data(&mut self, data: Bytes);
/// Need read data
fn need_read(&self) -> PayloadStatus;
}
/// Sender part of the payload stream /// Sender part of the payload stream
pub struct PayloadSender { pub struct PayloadSender {
inner: Weak<RefCell<Inner>>, inner: Weak<RefCell<Inner>>,
} }
impl PayloadWriter for PayloadSender { impl PayloadSender {
#[inline] #[inline]
fn set_error(&mut self, err: PayloadError) { pub fn set_error(&mut self, err: PayloadError) {
if let Some(shared) = self.inner.upgrade() { if let Some(shared) = self.inner.upgrade() {
shared.borrow_mut().set_error(err) shared.borrow_mut().set_error(err)
} }
} }
#[inline] #[inline]
fn feed_eof(&mut self) { pub fn feed_eof(&mut self) {
if let Some(shared) = self.inner.upgrade() { if let Some(shared) = self.inner.upgrade() {
shared.borrow_mut().feed_eof() shared.borrow_mut().feed_eof()
} }
} }
#[inline] #[inline]
fn feed_data(&mut self, data: Bytes) { pub fn feed_data(&mut self, data: Bytes) {
if let Some(shared) = self.inner.upgrade() { if let Some(shared) = self.inner.upgrade() {
shared.borrow_mut().feed_data(data) shared.borrow_mut().feed_data(data)
} }
} }
#[inline] #[inline]
fn need_read(&self) -> PayloadStatus { pub fn need_read(&self, cx: &mut Context<'_>) -> PayloadStatus {
// we check need_read only if Payload (other side) is alive, // we check need_read only if Payload (other side) is alive,
// otherwise always return true (consume payload) // otherwise always return true (consume payload)
if let Some(shared) = self.inner.upgrade() { if let Some(shared) = self.inner.upgrade() {
if shared.borrow().need_read { if shared.borrow().need_read {
PayloadStatus::Read PayloadStatus::Read
} else { } else {
#[cfg(not(test))] shared.borrow_mut().io_task.register(cx.waker());
{
if shared.borrow_mut().io_task.is_none() {
shared.borrow_mut().io_task = Some(current_task());
}
}
PayloadStatus::Pause PayloadStatus::Pause
} }
} else { } else {
@ -177,9 +150,8 @@ struct Inner {
err: Option<PayloadError>, err: Option<PayloadError>,
need_read: bool, need_read: bool,
items: VecDeque<Bytes>, items: VecDeque<Bytes>,
capacity: usize, task: LocalWaker,
task: Option<Task>, io_task: LocalWaker,
io_task: Option<Task>,
} }
impl Inner { impl Inner {
@ -190,9 +162,8 @@ impl Inner {
err: None, err: None,
items: VecDeque::new(), items: VecDeque::new(),
need_read: true, need_read: true,
capacity: MAX_BUFFER_SIZE, task: LocalWaker::new(),
task: None, io_task: LocalWaker::new(),
io_task: None,
} }
} }
@ -210,9 +181,9 @@ impl Inner {
fn feed_data(&mut self, data: Bytes) { fn feed_data(&mut self, data: Bytes) {
self.len += data.len(); self.len += data.len();
self.items.push_back(data); self.items.push_back(data);
self.need_read = self.len < self.capacity; self.need_read = self.len < MAX_BUFFER_SIZE;
if let Some(task) = self.task.take() { if let Some(task) = self.task.take() {
task.notify() task.wake()
} }
} }
@ -221,34 +192,28 @@ impl Inner {
self.len self.len
} }
fn readany(&mut self) -> Poll<Option<Bytes>, PayloadError> { fn readany(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, PayloadError>>> {
if let Some(data) = self.items.pop_front() { if let Some(data) = self.items.pop_front() {
self.len -= data.len(); self.len -= data.len();
self.need_read = self.len < self.capacity; self.need_read = self.len < MAX_BUFFER_SIZE;
if self.need_read && self.task.is_none() && !self.eof { if self.need_read && !self.eof {
self.task = Some(current_task()); self.task.register(cx.waker());
} }
if let Some(task) = self.io_task.take() { self.io_task.wake();
task.notify() Poll::Ready(Some(Ok(data)))
}
Ok(Async::Ready(Some(data)))
} else if let Some(err) = self.err.take() { } else if let Some(err) = self.err.take() {
Err(err) Poll::Ready(Some(Err(err)))
} else if self.eof { } else if self.eof {
Ok(Async::Ready(None)) Poll::Ready(None)
} else { } else {
self.need_read = true; self.need_read = true;
#[cfg(not(test))] self.task.register(cx.waker());
{ self.io_task.wake();
if self.task.is_none() { Poll::Pending
self.task = Some(current_task());
}
if let Some(task) = self.io_task.take() {
task.notify()
}
}
Ok(Async::NotReady)
} }
} }
@ -258,426 +223,22 @@ impl Inner {
} }
} }
/// Payload buffer
pub struct PayloadBuffer<S> {
len: usize,
items: VecDeque<Bytes>,
stream: S,
}
impl<S> PayloadBuffer<S>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
/// Create new `PayloadBuffer` instance
pub fn new(stream: S) -> Self {
PayloadBuffer {
len: 0,
items: VecDeque::new(),
stream,
}
}
/// Get mutable reference to an inner stream.
pub fn get_mut(&mut self) -> &mut S {
&mut self.stream
}
#[inline]
fn poll_stream(&mut self) -> Poll<bool, PayloadError> {
self.stream.poll().map(|res| match res {
Async::Ready(Some(data)) => {
self.len += data.len();
self.items.push_back(data);
Async::Ready(true)
}
Async::Ready(None) => Async::Ready(false),
Async::NotReady => Async::NotReady,
})
}
/// Read first available chunk of bytes
#[inline]
pub fn readany(&mut self) -> Poll<Option<Bytes>, PayloadError> {
if let Some(data) = self.items.pop_front() {
self.len -= data.len();
Ok(Async::Ready(Some(data)))
} else {
match self.poll_stream()? {
Async::Ready(true) => self.readany(),
Async::Ready(false) => Ok(Async::Ready(None)),
Async::NotReady => Ok(Async::NotReady),
}
}
}
/// Check if buffer contains enough bytes
#[inline]
pub fn can_read(&mut self, size: usize) -> Poll<Option<bool>, PayloadError> {
if size <= self.len {
Ok(Async::Ready(Some(true)))
} else {
match self.poll_stream()? {
Async::Ready(true) => self.can_read(size),
Async::Ready(false) => Ok(Async::Ready(None)),
Async::NotReady => Ok(Async::NotReady),
}
}
}
/// Return reference to the first chunk of data
#[inline]
pub fn get_chunk(&mut self) -> Poll<Option<&[u8]>, PayloadError> {
if self.items.is_empty() {
match self.poll_stream()? {
Async::Ready(true) => (),
Async::Ready(false) => return Ok(Async::Ready(None)),
Async::NotReady => return Ok(Async::NotReady),
}
}
match self.items.front().map(|c| c.as_ref()) {
Some(chunk) => Ok(Async::Ready(Some(chunk))),
None => Ok(Async::NotReady),
}
}
/// Read exact number of bytes
#[inline]
pub fn read_exact(&mut self, size: usize) -> Poll<Option<Bytes>, PayloadError> {
if size <= self.len {
self.len -= size;
let mut chunk = self.items.pop_front().unwrap();
if size < chunk.len() {
let buf = chunk.split_to(size);
self.items.push_front(chunk);
Ok(Async::Ready(Some(buf)))
} else if size == chunk.len() {
Ok(Async::Ready(Some(chunk)))
} else {
let mut buf = BytesMut::with_capacity(size);
buf.extend_from_slice(&chunk);
while buf.len() < size {
let mut chunk = self.items.pop_front().unwrap();
let rem = cmp::min(size - buf.len(), chunk.len());
buf.extend_from_slice(&chunk.split_to(rem));
if !chunk.is_empty() {
self.items.push_front(chunk);
}
}
Ok(Async::Ready(Some(buf.freeze())))
}
} else {
match self.poll_stream()? {
Async::Ready(true) => self.read_exact(size),
Async::Ready(false) => Ok(Async::Ready(None)),
Async::NotReady => Ok(Async::NotReady),
}
}
}
/// Remove specified amount if bytes from buffer
#[inline]
pub fn drop_bytes(&mut self, size: usize) {
if size <= self.len {
self.len -= size;
let mut len = 0;
while len < size {
let mut chunk = self.items.pop_front().unwrap();
let rem = cmp::min(size - len, chunk.len());
len += rem;
if rem < chunk.len() {
chunk.split_to(rem);
self.items.push_front(chunk);
}
}
}
}
/// Copy buffered data
pub fn copy(&mut self, size: usize) -> Poll<Option<BytesMut>, PayloadError> {
if size <= self.len {
let mut buf = BytesMut::with_capacity(size);
for chunk in &self.items {
if buf.len() < size {
let rem = cmp::min(size - buf.len(), chunk.len());
buf.extend_from_slice(&chunk[..rem]);
}
if buf.len() == size {
return Ok(Async::Ready(Some(buf)));
}
}
}
match self.poll_stream()? {
Async::Ready(true) => self.copy(size),
Async::Ready(false) => Ok(Async::Ready(None)),
Async::NotReady => Ok(Async::NotReady),
}
}
/// Read until specified ending
pub fn read_until(&mut self, line: &[u8]) -> Poll<Option<Bytes>, PayloadError> {
let mut idx = 0;
let mut num = 0;
let mut offset = 0;
let mut found = false;
let mut length = 0;
for no in 0..self.items.len() {
{
let chunk = &self.items[no];
for (pos, ch) in chunk.iter().enumerate() {
if *ch == line[idx] {
idx += 1;
if idx == line.len() {
num = no;
offset = pos + 1;
length += pos + 1;
found = true;
break;
}
} else {
idx = 0
}
}
if !found {
length += chunk.len()
}
}
if found {
let mut buf = BytesMut::with_capacity(length);
if num > 0 {
for _ in 0..num {
buf.extend_from_slice(&self.items.pop_front().unwrap());
}
}
if offset > 0 {
let mut chunk = self.items.pop_front().unwrap();
buf.extend_from_slice(&chunk.split_to(offset));
if !chunk.is_empty() {
self.items.push_front(chunk)
}
}
self.len -= length;
return Ok(Async::Ready(Some(buf.freeze())));
}
}
match self.poll_stream()? {
Async::Ready(true) => self.read_until(line),
Async::Ready(false) => Ok(Async::Ready(None)),
Async::NotReady => Ok(Async::NotReady),
}
}
/// Read bytes until new line delimiter
pub fn readline(&mut self) -> Poll<Option<Bytes>, PayloadError> {
self.read_until(b"\n")
}
/// Put unprocessed data back to the buffer
pub fn unprocessed(&mut self, data: Bytes) {
self.len += data.len();
self.items.push_front(data);
}
/// Get remaining data from the buffer
pub fn remaining(&mut self) -> Bytes {
self.items
.iter_mut()
.fold(BytesMut::new(), |mut b, c| {
b.extend_from_slice(c);
b
})
.freeze()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use actix_rt::Runtime; use futures_util::future::poll_fn;
use futures::future::{lazy, result};
#[test] #[actix_rt::test]
fn test_basic() { async fn test_unread_data() {
Runtime::new() let (_, mut payload) = Payload::create(false);
.unwrap()
.block_on(lazy(|| {
let (_, payload) = Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(payload.len, 0); payload.unread_data(Bytes::from("data"));
assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); assert!(!payload.is_empty());
assert_eq!(payload.len(), 4);
let res: Result<(), ()> = Ok(()); assert_eq!(
result(res) Bytes::from("data"),
})) poll_fn(|cx| payload.readany(cx)).await.unwrap().unwrap()
.unwrap(); );
}
#[test]
fn test_eof() {
Runtime::new()
.unwrap()
.block_on(lazy(|| {
let (mut sender, payload) = Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(Async::NotReady, payload.readany().ok().unwrap());
sender.feed_data(Bytes::from("data"));
sender.feed_eof();
assert_eq!(
Async::Ready(Some(Bytes::from("data"))),
payload.readany().ok().unwrap()
);
assert_eq!(payload.len, 0);
assert_eq!(Async::Ready(None), payload.readany().ok().unwrap());
let res: Result<(), ()> = Ok(());
result(res)
}))
.unwrap();
}
#[test]
fn test_err() {
Runtime::new()
.unwrap()
.block_on(lazy(|| {
let (mut sender, payload) = Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(Async::NotReady, payload.readany().ok().unwrap());
sender.set_error(PayloadError::Incomplete(None));
payload.readany().err().unwrap();
let res: Result<(), ()> = Ok(());
result(res)
}))
.unwrap();
}
#[test]
fn test_readany() {
Runtime::new()
.unwrap()
.block_on(lazy(|| {
let (mut sender, payload) = Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
sender.feed_data(Bytes::from("line1"));
sender.feed_data(Bytes::from("line2"));
assert_eq!(
Async::Ready(Some(Bytes::from("line1"))),
payload.readany().ok().unwrap()
);
assert_eq!(payload.len, 0);
assert_eq!(
Async::Ready(Some(Bytes::from("line2"))),
payload.readany().ok().unwrap()
);
assert_eq!(payload.len, 0);
let res: Result<(), ()> = Ok(());
result(res)
}))
.unwrap();
}
#[test]
fn test_readexactly() {
Runtime::new()
.unwrap()
.block_on(lazy(|| {
let (mut sender, payload) = Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(Async::NotReady, payload.read_exact(2).ok().unwrap());
sender.feed_data(Bytes::from("line1"));
sender.feed_data(Bytes::from("line2"));
assert_eq!(
Async::Ready(Some(Bytes::from_static(b"li"))),
payload.read_exact(2).ok().unwrap()
);
assert_eq!(payload.len, 3);
assert_eq!(
Async::Ready(Some(Bytes::from_static(b"ne1l"))),
payload.read_exact(4).ok().unwrap()
);
assert_eq!(payload.len, 4);
sender.set_error(PayloadError::Incomplete(None));
payload.read_exact(10).err().unwrap();
let res: Result<(), ()> = Ok(());
result(res)
}))
.unwrap();
}
#[test]
fn test_readuntil() {
Runtime::new()
.unwrap()
.block_on(lazy(|| {
let (mut sender, payload) = Payload::create(false);
let mut payload = PayloadBuffer::new(payload);
assert_eq!(Async::NotReady, payload.read_until(b"ne").ok().unwrap());
sender.feed_data(Bytes::from("line1"));
sender.feed_data(Bytes::from("line2"));
assert_eq!(
Async::Ready(Some(Bytes::from("line"))),
payload.read_until(b"ne").ok().unwrap()
);
assert_eq!(payload.len, 1);
assert_eq!(
Async::Ready(Some(Bytes::from("1line2"))),
payload.read_until(b"2").ok().unwrap()
);
assert_eq!(payload.len, 0);
sender.set_error(PayloadError::Incomplete(None));
payload.read_until(b"b").err().unwrap();
let res: Result<(), ()> = Ok(());
result(res)
}))
.unwrap();
}
#[test]
fn test_unread_data() {
Runtime::new()
.unwrap()
.block_on(lazy(|| {
let (_, mut payload) = Payload::create(false);
payload.unread_data(Bytes::from("data"));
assert!(!payload.is_empty());
assert_eq!(payload.len(), 4);
assert_eq!(
Async::Ready(Some(Bytes::from("data"))),
payload.poll().ok().unwrap()
);
let res: Result<(), ()> = Ok(());
result(res)
}))
.unwrap();
} }
} }

View File

@ -1,81 +1,279 @@
use std::fmt::Debug; use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use std::{fmt, net};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_server_config::{Io, ServerConfig as SrvConfig}; use actix_rt::net::TcpStream;
use actix_service::{IntoNewService, NewService, Service}; use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory};
use actix_utils::cloneable::CloneableService; use futures_core::ready;
use futures::future::{ok, FutureResult}; use futures_util::future::{ok, Ready};
use futures::{try_ready, Async, Future, IntoFuture, Poll, Stream};
use crate::body::MessageBody; use crate::body::MessageBody;
use crate::config::{KeepAlive, ServiceConfig}; use crate::cloneable::CloneableService;
use crate::error::{DispatchError, ParseError}; use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error, ParseError};
use crate::helpers::DataFactory;
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use super::codec::Codec; use super::codec::Codec;
use super::dispatcher::Dispatcher; use super::dispatcher::Dispatcher;
use super::Message; use super::{ExpectHandler, Message, UpgradeHandler};
/// `NewService` implementation for HTTP1 transport /// `ServiceFactory` implementation for HTTP1 transport
pub struct H1Service<T, P, S, B> { pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler<T>> {
srv: S, srv: S,
cfg: ServiceConfig, cfg: ServiceConfig,
_t: PhantomData<(T, P, B)>, expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
_t: PhantomData<(T, B)>,
} }
impl<T, P, S, B> H1Service<T, P, S, B> impl<T, S, B> H1Service<T, S, B>
where where
S: NewService<SrvConfig, Request = Request>, S: ServiceFactory<Config = (), Request = Request>,
S::Error: Debug, S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::Service: 'static,
B: MessageBody, B: MessageBody,
{ {
/// Create new `HttpService` instance with default config.
pub fn new<F: IntoNewService<S, SrvConfig>>(service: F) -> Self {
let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0);
H1Service {
cfg,
srv: service.into_new_service(),
_t: PhantomData,
}
}
/// Create new `HttpService` instance with config. /// Create new `HttpService` instance with config.
pub fn with_config<F: IntoNewService<S, SrvConfig>>( pub(crate) fn with_config<F: IntoServiceFactory<S>>(
cfg: ServiceConfig, cfg: ServiceConfig,
service: F, service: F,
) -> Self { ) -> Self {
H1Service { H1Service {
cfg, cfg,
srv: service.into_new_service(), srv: service.into_factory(),
expect: ExpectHandler,
upgrade: None,
on_connect: None,
_t: PhantomData, _t: PhantomData,
} }
} }
} }
impl<T, P, S, B> NewService<SrvConfig> for H1Service<T, P, S, B> impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
where where
T: AsyncRead + AsyncWrite, S: ServiceFactory<Config = (), Request = Request>,
S: NewService<SrvConfig, Request = Request>, S::Error: Into<Error>,
S::Error: Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::Service: 'static, B: MessageBody,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<
Config = (),
Request = (Request, Framed<TcpStream, Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
{
/// Create simple tcp stream service
pub fn tcp(
self,
) -> impl ServiceFactory<
Config = (),
Request = TcpStream,
Response = (),
Error = DispatchError,
InitError = (),
> {
pipeline_factory(|io: TcpStream| {
let peer_addr = io.peer_addr().ok();
ok((io, peer_addr))
})
.and_then(self)
}
}
#[cfg(feature = "openssl")]
mod openssl {
use super::*;
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
use actix_tls::{openssl::HandshakeError, SslError};
impl<S, B, X, U> H1Service<SslStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<
Config = (),
Request = (Request, Framed<SslStream<TcpStream>, Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
{
/// Create openssl based service
pub fn openssl(
self,
acceptor: SslAcceptor,
) -> impl ServiceFactory<
Config = (),
Request = TcpStream,
Response = (),
Error = SslError<HandshakeError<TcpStream>, DispatchError>,
InitError = (),
> {
pipeline_factory(
Acceptor::new(acceptor)
.map_err(SslError::Ssl)
.map_init_err(|_| panic!()),
)
.and_then(|io: SslStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok();
ok((io, peer_addr))
})
.and_then(self.map_err(SslError::Service))
}
}
}
#[cfg(feature = "rustls")]
mod rustls {
use super::*;
use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::SslError;
use std::{fmt, io};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<
Config = (),
Request = (Request, Framed<TlsStream<TcpStream>, Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
{
/// Create rustls based service
pub fn rustls(
self,
config: ServerConfig,
) -> impl ServiceFactory<
Config = (),
Request = TcpStream,
Response = (),
Error = SslError<io::Error, DispatchError>,
InitError = (),
> {
pipeline_factory(
Acceptor::new(config)
.map_err(SslError::Ssl)
.map_init_err(|_| panic!()),
)
.and_then(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
ok((io, peer_addr))
})
.and_then(self.map_err(SslError::Service))
}
}
}
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
where
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
{ {
type Request = Io<T, P>; pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
where
X1: ServiceFactory<Request = Request, Response = Request>,
X1::Error: Into<Error>,
X1::InitError: fmt::Debug,
{
H1Service {
expect,
cfg: self.cfg,
srv: self.srv,
upgrade: self.upgrade,
on_connect: self.on_connect,
_t: PhantomData,
}
}
pub fn upgrade<U1>(self, upgrade: Option<U1>) -> H1Service<T, S, B, X, U1>
where
U1: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U1::Error: fmt::Display,
U1::InitError: fmt::Debug,
{
H1Service {
upgrade,
cfg: self.cfg,
srv: self.srv,
expect: self.expect,
on_connect: self.on_connect,
_t: PhantomData,
}
}
/// Set on connect callback.
pub(crate) fn on_connect(
mut self,
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self {
self.on_connect = f;
self
}
}
impl<T, S, B, X, U> ServiceFactory for H1Service<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
S::InitError: fmt::Debug,
B: MessageBody,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
{
type Config = ();
type Request = (T, Option<net::SocketAddr>);
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type InitError = S::InitError; type InitError = ();
type Service = H1ServiceHandler<T, P, S::Service, B>; type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
type Future = H1ServiceResponse<T, P, S, B>; type Future = H1ServiceResponse<T, S, B, X, U>;
fn new_service(&self, cfg: &SrvConfig) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
H1ServiceResponse { H1ServiceResponse {
fut: self.srv.new_service(cfg).into_future(), fut: self.srv.new_service(()),
fut_ex: Some(self.expect.new_service(())),
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())),
expect: None,
upgrade: None,
on_connect: self.on_connect.clone(),
cfg: Some(self.cfg.clone()), cfg: Some(self.cfg.clone()),
_t: PhantomData, _t: PhantomData,
} }
@ -83,91 +281,215 @@ where
} }
#[doc(hidden)] #[doc(hidden)]
pub struct H1ServiceResponse<T, P, S: NewService<SrvConfig, Request = Request>, B> { #[pin_project::pin_project]
fut: <S::Future as IntoFuture>::Future, pub struct H1ServiceResponse<T, S, B, X, U>
where
S: ServiceFactory<Request = Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
X: ServiceFactory<Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<Request = (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: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
cfg: Option<ServiceConfig>, cfg: Option<ServiceConfig>,
_t: PhantomData<(T, P, B)>, _t: PhantomData<(T, B)>,
} }
impl<T, P, S, B> Future for H1ServiceResponse<T, P, S, B> impl<T, S, B, X, U> Future for H1ServiceResponse<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
S: NewService<SrvConfig, Request = Request>, S: ServiceFactory<Request = Request>,
S::Service: 'static, S::Error: Into<Error>,
S::Error: Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
X: ServiceFactory<Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{ {
type Item = H1ServiceHandler<T, P, S::Service, B>; type Output = Result<H1ServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
type Error = S::InitError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let service = try_ready!(self.fut.poll()); let mut this = self.as_mut().project();
Ok(Async::Ready(H1ServiceHandler::new(
self.cfg.take().unwrap(), if let Some(fut) = this.fut_ex.as_pin_mut() {
service, let expect = ready!(fut
))) .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_ex.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,
this.expect.take().unwrap(),
this.upgrade.take(),
this.on_connect.clone(),
)
}))
} }
} }
/// `Service` implementation for HTTP1 transport /// `Service` implementation for HTTP1 transport
pub struct H1ServiceHandler<T, P, S: 'static, B> { pub struct H1ServiceHandler<T, S, B, X, U> {
srv: CloneableService<S>, srv: CloneableService<S>,
expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
cfg: ServiceConfig, cfg: ServiceConfig,
_t: PhantomData<(T, P, B)>, _t: PhantomData<(T, B)>,
} }
impl<T, P, S, B> H1ServiceHandler<T, P, S, B> impl<T, S, B, X, U> H1ServiceHandler<T, S, B, X, U>
where where
S: Service<Request = Request>, S: Service<Request = Request>,
S::Error: Debug, S::Error: Into<Error>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
{ {
fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler<T, P, S, B> { fn new(
cfg: ServiceConfig,
srv: S,
expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> H1ServiceHandler<T, S, B, X, U> {
H1ServiceHandler { H1ServiceHandler {
srv: CloneableService::new(srv), srv: CloneableService::new(srv),
expect: CloneableService::new(expect),
upgrade: upgrade.map(CloneableService::new),
cfg, cfg,
on_connect,
_t: PhantomData, _t: PhantomData,
} }
} }
} }
impl<T, P, S, B> Service for H1ServiceHandler<T, P, S, B> impl<T, S, B, X, U> Service for H1ServiceHandler<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request>, S: Service<Request = Request>,
S::Error: Debug, S::Error: Into<Error>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>,
{ {
type Request = Io<T, P>; type Request = (T, Option<net::SocketAddr>);
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Future = Dispatcher<T, S, B>; type Future = Dispatcher<T, S, B, X, U>;
fn poll_ready(&mut self) -> Poll<(), Self::Error> { fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.srv.poll_ready().map_err(|e| { let ready = self
log::error!("Http service readiness error: {:?}", e); .expect
DispatchError::Service .poll_ready(cx)
}) .map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready();
let ready = self
.srv
.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) = self.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, req: Self::Request) -> Self::Future { fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
Dispatcher::new(req.into_parts().0, self.cfg.clone(), self.srv.clone()) let on_connect = if let Some(ref on_connect) = self.on_connect {
Some(on_connect(&io))
} else {
None
};
Dispatcher::new(
io,
self.cfg.clone(),
self.srv.clone(),
self.expect.clone(),
self.upgrade.clone(),
on_connect,
addr,
)
} }
} }
/// `NewService` implementation for `OneRequestService` service /// `ServiceFactory` implementation for `OneRequestService` service
#[derive(Default)] #[derive(Default)]
pub struct OneRequest<T, P> { pub struct OneRequest<T> {
config: ServiceConfig, config: ServiceConfig,
_t: PhantomData<(T, P)>, _t: PhantomData<T>,
} }
impl<T, P> OneRequest<T, P> impl<T> OneRequest<T>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
{ {
/// Create new `H1SimpleService` instance. /// Create new `H1SimpleService` instance.
pub fn new() -> Self { pub fn new() -> Self {
@ -178,51 +500,49 @@ where
} }
} }
impl<T, P> NewService<SrvConfig> for OneRequest<T, P> impl<T> ServiceFactory for OneRequest<T>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
{ {
type Request = Io<T, P>; type Config = ();
type Request = T;
type Response = (Request, Framed<T, Codec>); type Response = (Request, Framed<T, Codec>);
type Error = ParseError; type Error = ParseError;
type InitError = (); type InitError = ();
type Service = OneRequestService<T, P>; type Service = OneRequestService<T>;
type Future = FutureResult<Self::Service, Self::InitError>; type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: &SrvConfig) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
ok(OneRequestService { ok(OneRequestService {
config: self.config.clone(),
_t: PhantomData, _t: PhantomData,
config: self.config.clone(),
}) })
} }
} }
/// `Service` implementation for HTTP1 transport. Reads one request and returns /// `Service` implementation for HTTP1 transport. Reads one request and returns
/// request and framed object. /// request and framed object.
pub struct OneRequestService<T, P> { pub struct OneRequestService<T> {
_t: PhantomData<T>,
config: ServiceConfig, config: ServiceConfig,
_t: PhantomData<(T, P)>,
} }
impl<T, P> Service for OneRequestService<T, P> impl<T> Service for OneRequestService<T>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
{ {
type Request = Io<T, P>; type Request = T;
type Response = (Request, Framed<T, Codec>); type Response = (Request, Framed<T, Codec>);
type Error = ParseError; type Error = ParseError;
type Future = OneRequestServiceResponse<T>; type Future = OneRequestServiceResponse<T>;
fn poll_ready(&mut self) -> Poll<(), Self::Error> { fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Ok(Async::Ready(())) Poll::Ready(Ok(()))
} }
fn call(&mut self, req: Self::Request) -> Self::Future { fn call(&mut self, req: Self::Request) -> Self::Future {
OneRequestServiceResponse { OneRequestServiceResponse {
framed: Some(Framed::new( framed: Some(Framed::new(req, Codec::new(self.config.clone()))),
req.into_parts().0,
Codec::new(self.config.clone()),
)),
} }
} }
} }
@ -230,28 +550,28 @@ where
#[doc(hidden)] #[doc(hidden)]
pub struct OneRequestServiceResponse<T> pub struct OneRequestServiceResponse<T>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
{ {
framed: Option<Framed<T, Codec>>, framed: Option<Framed<T, Codec>>,
} }
impl<T> Future for OneRequestServiceResponse<T> impl<T> Future for OneRequestServiceResponse<T>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
{ {
type Item = (Request, Framed<T, Codec>); type Output = Result<(Request, Framed<T, Codec>), ParseError>;
type Error = ParseError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.framed.as_mut().unwrap().poll()? { match self.framed.as_mut().unwrap().next_item(cx) {
Async::Ready(Some(req)) => match req { Poll::Ready(Some(Ok(req))) => match req {
Message::Item(req) => { Message::Item(req) => {
Ok(Async::Ready((req, self.framed.take().unwrap()))) Poll::Ready(Ok((req, self.framed.take().unwrap())))
} }
Message::Chunk(_) => unreachable!("Something is wrong"), Message::Chunk(_) => unreachable!("Something is wrong"),
}, },
Async::Ready(None) => Err(ParseError::Incomplete), Poll::Ready(Some(Err(err))) => Poll::Ready(Err(err)),
Async::NotReady => Ok(Async::NotReady), Poll::Ready(None) => Poll::Ready(Err(ParseError::Incomplete)),
Poll::Pending => Poll::Pending,
} }
} }
} }

View File

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

View File

@ -0,0 +1,97 @@
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
use crate::body::{BodySize, MessageBody, ResponseBody};
use crate::error::Error;
use crate::h1::{Codec, Message};
use crate::response::Response;
/// Send http/1 response
#[pin_project::pin_project]
pub struct SendResponse<T, B> {
res: Option<Message<(Response<()>, BodySize)>>,
body: Option<ResponseBody<B>>,
framed: Option<Framed<T, Codec>>,
}
impl<T, B> SendResponse<T, B>
where
B: MessageBody,
{
pub fn new(framed: Framed<T, Codec>, response: Response<B>) -> Self {
let (res, body) = response.into_parts();
SendResponse {
res: Some((res, body.size()).into()),
body: Some(body),
framed: Some(framed),
}
}
}
impl<T, B> Future for SendResponse<T, B>
where
T: AsyncRead + AsyncWrite,
B: MessageBody,
{
type Output = Result<Framed<T, Codec>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
loop {
let mut body_ready = this.body.is_some();
let framed = this.framed.as_mut().unwrap();
// send body
if this.res.is_none() && this.body.is_some() {
while body_ready && this.body.is_some() && !framed.is_write_buf_full() {
match this.body.as_mut().unwrap().poll_next(cx)? {
Poll::Ready(item) => {
// body is done
if item.is_none() {
let _ = this.body.take();
}
framed.write(Message::Chunk(item))?;
}
Poll::Pending => body_ready = false,
}
}
}
// flush write buffer
if !framed.is_write_buf_empty() {
match framed.flush(cx)? {
Poll::Ready(_) => {
if body_ready {
continue;
} else {
return Poll::Pending;
}
}
Poll::Pending => return Poll::Pending,
}
}
// send response
if let Some(res) = this.res.take() {
framed.write(res)?;
continue;
}
if this.body.is_some() {
if body_ready {
continue;
} else {
return Poll::Pending;
}
} else {
break;
}
}
Poll::Ready(Ok(this.framed.take().unwrap()))
}
}

View File

@ -1,26 +1,25 @@
use std::collections::VecDeque; use std::convert::TryFrom;
use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::time::Instant; use std::net;
use std::{fmt, mem}; use std::pin::Pin;
use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{Delay, Instant};
use actix_service::Service; use actix_service::Service;
use actix_utils::cloneable::CloneableService;
use bitflags::bitflags;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures::{try_ready, Async, Future, Poll, Sink, Stream};
use h2::server::{Connection, SendResponse}; use h2::server::{Connection, SendResponse};
use h2::{RecvStream, SendStream}; use h2::SendStream;
use http::header::{ use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
HeaderValue, ACCEPT_ENCODING, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, use log::{error, trace};
};
use http::HttpTryFrom;
use log::{debug, error, trace};
use tokio_timer::Delay;
use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::body::{BodySize, MessageBody, ResponseBody};
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error, ParseError, PayloadError, ResponseError}; use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::httpmessage::HttpMessage;
use crate::message::ResponseHead; use crate::message::ResponseHead;
use crate::payload::Payload; use crate::payload::Payload;
use crate::request::Request; use crate::request::Request;
@ -29,14 +28,16 @@ use crate::response::Response;
const CHUNK_SIZE: usize = 16_384; const CHUNK_SIZE: usize = 16_384;
/// Dispatcher for HTTP/2 protocol /// Dispatcher for HTTP/2 protocol
pub struct Dispatcher< #[pin_project::pin_project]
T: AsyncRead + AsyncWrite, pub struct Dispatcher<T, S: Service<Request = Request>, B: MessageBody>
S: Service<Request = Request> + 'static, where
B: MessageBody, T: AsyncRead + AsyncWrite + Unpin,
> { {
service: CloneableService<S>, service: CloneableService<S>,
connection: Connection<T, Bytes>, connection: Connection<T, Bytes>,
on_connect: Option<Box<dyn DataFactory>>,
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>,
ka_expire: Instant, ka_expire: Instant,
ka_timer: Option<Delay>, ka_timer: Option<Delay>,
_t: PhantomData<B>, _t: PhantomData<B>,
@ -44,17 +45,20 @@ pub struct Dispatcher<
impl<T, S, B> Dispatcher<T, S, B> impl<T, S, B> Dispatcher<T, S, B>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request> + 'static, S: Service<Request = Request>,
S::Error: fmt::Debug, S::Error: Into<Error>,
// S::Future: 'static,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody + 'static, B: MessageBody,
{ {
pub fn new( pub(crate) fn new(
service: CloneableService<S>, service: CloneableService<S>,
connection: Connection<T, Bytes>, connection: Connection<T, Bytes>,
on_connect: Option<Box<dyn DataFactory>>,
config: ServiceConfig, config: ServiceConfig,
timeout: Option<Delay>, timeout: Option<Delay>,
peer_addr: Option<net::SocketAddr>,
) -> Self { ) -> Self {
// let keepalive = config.keep_alive_enabled(); // let keepalive = config.keep_alive_enabled();
// let flags = if keepalive { // let flags = if keepalive {
@ -75,9 +79,11 @@ where
Dispatcher { Dispatcher {
service, service,
config, config,
peer_addr,
connection,
on_connect,
ka_expire, ka_expire,
ka_timer, ka_timer,
connection,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -85,76 +91,98 @@ where
impl<T, S, B> Future for Dispatcher<T, S, B> impl<T, S, B> Future for Dispatcher<T, S, B>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request> + 'static, S: Service<Request = Request>,
S::Error: fmt::Debug, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>>, S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
type Item = (); type Output = Result<(), DispatchError>;
type Error = DispatchError;
#[inline] #[inline]
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
loop { loop {
match self.connection.poll()? { match Pin::new(&mut this.connection).poll_accept(cx) {
Async::Ready(None) => return Ok(Async::Ready(())), Poll::Ready(None) => return Poll::Ready(Ok(())),
Async::Ready(Some((req, res))) => { Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())),
Poll::Ready(Some(Ok((req, res)))) => {
// update keep-alive expire // update keep-alive expire
if self.ka_timer.is_some() { if this.ka_timer.is_some() {
if let Some(expire) = self.config.keep_alive_expire() { if let Some(expire) = this.config.keep_alive_expire() {
self.ka_expire = expire; this.ka_expire = expire;
} }
} }
let (parts, body) = req.into_parts(); let (parts, body) = req.into_parts();
let mut req = Request::with_payload(body.into()); let mut req = Request::with_payload(Payload::<
crate::payload::PayloadStream,
>::H2(
crate::h2::Payload::new(body)
));
let head = &mut req.head_mut(); let head = &mut req.head_mut();
head.uri = parts.uri; head.uri = parts.uri;
head.method = parts.method; head.method = parts.method;
head.version = parts.version; head.version = parts.version;
head.headers = parts.headers; head.headers = parts.headers.into();
tokio_current_thread::spawn(ServiceResponse::<S, B> { head.peer_addr = this.peer_addr;
// set on_connect data
if let Some(ref on_connect) = this.on_connect {
on_connect.set(&mut req.extensions_mut());
}
actix_rt::spawn(ServiceResponse::<
S::Future,
S::Response,
S::Error,
B,
> {
state: ServiceResponseState::ServiceCall( state: ServiceResponseState::ServiceCall(
self.service.call(req), this.service.call(req),
Some(res), Some(res),
), ),
config: self.config.clone(), config: this.config.clone(),
buffer: None, buffer: None,
}) _t: PhantomData,
});
} }
Async::NotReady => return Ok(Async::NotReady), Poll::Pending => return Poll::Pending,
} }
} }
} }
} }
struct ServiceResponse<S: Service, B> { #[pin_project::pin_project]
state: ServiceResponseState<S, B>, struct ServiceResponse<F, I, E, B> {
state: ServiceResponseState<F, B>,
config: ServiceConfig, config: ServiceConfig,
buffer: Option<Bytes>, buffer: Option<Bytes>,
_t: PhantomData<(I, E)>,
} }
enum ServiceResponseState<S: Service, B> { enum ServiceResponseState<F, B> {
ServiceCall(S::Future, Option<SendResponse<Bytes>>), ServiceCall(F, Option<SendResponse<Bytes>>),
SendPayload(SendStream<Bytes>, ResponseBody<B>), SendPayload(SendStream<Bytes>, ResponseBody<B>),
} }
impl<S, B> ServiceResponse<S, B> impl<F, I, E, B> ServiceResponse<F, I, E, B>
where where
S: Service<Request = Request> + 'static, F: Future<Output = Result<I, E>>,
S::Error: fmt::Debug, E: Into<Error>,
S::Response: Into<Response<B>>, I: Into<Response<B>>,
B: MessageBody + 'static, B: MessageBody,
{ {
fn prepare_response( fn prepare_response(
&self, &self,
head: &ResponseHead, head: &ResponseHead,
length: &mut BodySize, size: &mut BodySize,
) -> http::Response<()> { ) -> http::Response<()> {
let mut has_date = false; let mut has_date = false;
let mut skip_len = length != &BodySize::Stream; let mut skip_len = size != &BodySize::Stream;
let mut res = http::Response::new(()); let mut res = http::Response::new(());
*res.status_mut() = head.status; *res.status_mut() = head.status;
@ -164,14 +192,14 @@ where
match head.status { match head.status {
http::StatusCode::NO_CONTENT http::StatusCode::NO_CONTENT
| http::StatusCode::CONTINUE | http::StatusCode::CONTINUE
| http::StatusCode::PROCESSING => *length = BodySize::None, | http::StatusCode::PROCESSING => *size = BodySize::None,
http::StatusCode::SWITCHING_PROTOCOLS => { http::StatusCode::SWITCHING_PROTOCOLS => {
skip_len = true; skip_len = true;
*length = BodySize::Stream; *size = BodySize::Stream;
} }
_ => (), _ => (),
} }
let _ = match length { let _ = match size {
BodySize::None | BodySize::Stream => None, BodySize::None | BodySize::Stream => None,
BodySize::Empty => res BodySize::Empty => res
.headers_mut() .headers_mut()
@ -201,119 +229,130 @@ where
if !has_date { if !has_date {
let mut bytes = BytesMut::with_capacity(29); let mut bytes = BytesMut::with_capacity(29);
self.config.set_date_header(&mut bytes); self.config.set_date_header(&mut bytes);
res.headers_mut() res.headers_mut().insert(DATE, unsafe {
.insert(DATE, HeaderValue::try_from(bytes.freeze()).unwrap()); HeaderValue::from_maybe_shared_unchecked(bytes.freeze())
});
} }
res res
} }
} }
impl<S, B> Future for ServiceResponse<S, B> impl<F, I, E, B> Future for ServiceResponse<F, I, E, B>
where where
S: Service<Request = Request> + 'static, F: Future<Output = Result<I, E>>,
S::Error: fmt::Debug, E: Into<Error>,
S::Response: Into<Response<B>>, I: Into<Response<B>>,
B: MessageBody + 'static, B: MessageBody,
{ {
type Item = (); type Output = ();
type Error = ();
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.state { let mut this = self.as_mut().project();
match this.state {
ServiceResponseState::ServiceCall(ref mut call, ref mut send) => { ServiceResponseState::ServiceCall(ref mut call, ref mut send) => {
match call.poll() { match unsafe { Pin::new_unchecked(call) }.poll(cx) {
Ok(Async::Ready(res)) => { Poll::Ready(Ok(res)) => {
let (res, body) = res.into().replace_body(()); let (res, body) = res.into().replace_body(());
let mut send = send.take().unwrap(); let mut send = send.take().unwrap();
let mut length = body.length(); let mut size = body.size();
let h2_res = self.prepare_response(res.head(), &mut length); let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = send let stream = match send.send_response(h2_res, size.is_eof()) {
.send_response(h2_res, length.is_eof()) Err(e) => {
.map_err(|e| {
trace!("Error sending h2 response: {:?}", e); trace!("Error sending h2 response: {:?}", e);
})?; return Poll::Ready(());
}
Ok(stream) => stream,
};
if length.is_eof() { if size.is_eof() {
Ok(Async::Ready(())) Poll::Ready(())
} else { } else {
self.state = ServiceResponseState::SendPayload(stream, body); *this.state =
self.poll() ServiceResponseState::SendPayload(stream, body);
self.poll(cx)
} }
} }
Ok(Async::NotReady) => Ok(Async::NotReady), Poll::Pending => Poll::Pending,
Err(_e) => { Poll::Ready(Err(e)) => {
let res: Response = Response::InternalServerError().finish(); let res: Response = e.into().into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
let mut send = send.take().unwrap(); let mut send = send.take().unwrap();
let mut length = body.length(); let mut size = body.size();
let h2_res = self.prepare_response(res.head(), &mut length); let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = send let stream = match send.send_response(h2_res, size.is_eof()) {
.send_response(h2_res, length.is_eof()) Err(e) => {
.map_err(|e| {
trace!("Error sending h2 response: {:?}", e); trace!("Error sending h2 response: {:?}", e);
})?; return Poll::Ready(());
}
Ok(stream) => stream,
};
if length.is_eof() { if size.is_eof() {
Ok(Async::Ready(())) Poll::Ready(())
} else { } else {
self.state = ServiceResponseState::SendPayload( *this.state = ServiceResponseState::SendPayload(
stream, stream,
body.into_body(), body.into_body(),
); );
self.poll() self.poll(cx)
} }
} }
} }
} }
ServiceResponseState::SendPayload(ref mut stream, ref mut body) => loop { ServiceResponseState::SendPayload(ref mut stream, ref mut body) => loop {
loop { loop {
if let Some(ref mut buffer) = self.buffer { if let Some(ref mut buffer) = this.buffer {
match stream.poll_capacity().map_err(|e| warn!("{:?}", e))? { match stream.poll_capacity(cx) {
Async::NotReady => return Ok(Async::NotReady), Poll::Pending => return Poll::Pending,
Async::Ready(None) => return Ok(Async::Ready(())), Poll::Ready(None) => return Poll::Ready(()),
Async::Ready(Some(cap)) => { Poll::Ready(Some(Ok(cap))) => {
let len = buffer.len(); let len = buffer.len();
let bytes = buffer.split_to(std::cmp::min(cap, len)); let bytes = buffer.split_to(std::cmp::min(cap, len));
if let Err(e) = stream.send_data(bytes, false) { if let Err(e) = stream.send_data(bytes, false) {
warn!("{:?}", e); warn!("{:?}", e);
return Err(()); return Poll::Ready(());
} else if !buffer.is_empty() { } else if !buffer.is_empty() {
let cap = std::cmp::min(buffer.len(), CHUNK_SIZE); let cap = std::cmp::min(buffer.len(), CHUNK_SIZE);
stream.reserve_capacity(cap); stream.reserve_capacity(cap);
} else { } else {
self.buffer.take(); this.buffer.take();
} }
} }
Poll::Ready(Some(Err(e))) => {
warn!("{:?}", e);
return Poll::Ready(());
}
} }
} else { } else {
match body.poll_next() { match body.poll_next(cx) {
Ok(Async::NotReady) => { Poll::Pending => return Poll::Pending,
return Ok(Async::NotReady); Poll::Ready(None) => {
}
Ok(Async::Ready(None)) => {
if let Err(e) = stream.send_data(Bytes::new(), true) { if let Err(e) = stream.send_data(Bytes::new(), true) {
warn!("{:?}", e); warn!("{:?}", e);
return Err(());
} else {
return Ok(Async::Ready(()));
} }
return Poll::Ready(());
} }
Ok(Async::Ready(Some(chunk))) => { Poll::Ready(Some(Ok(chunk))) => {
stream.reserve_capacity(std::cmp::min( stream.reserve_capacity(std::cmp::min(
chunk.len(), chunk.len(),
CHUNK_SIZE, CHUNK_SIZE,
)); ));
self.buffer = Some(chunk); *this.buffer = Some(chunk);
} }
Err(e) => { Poll::Ready(Some(Err(e))) => {
error!("Response payload stream error: {:?}", e); error!("Response payload stream error: {:?}", e);
return Err(()); return Poll::Ready(());
} }
} }
} }

View File

@ -1,9 +1,9 @@
#![allow(dead_code, unused_imports)] //! HTTP/2 implementation
use std::pin::Pin;
use std::fmt; use std::task::{Context, Poll};
use bytes::Bytes; use bytes::Bytes;
use futures::{Async, Poll, Stream}; use futures_core::Stream;
use h2::RecvStream; use h2::RecvStream;
mod dispatcher; mod dispatcher;
@ -25,22 +25,26 @@ impl Payload {
} }
impl Stream for Payload { impl Stream for Payload {
type Item = Bytes; type Item = Result<Bytes, PayloadError>;
type Error = PayloadError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { fn poll_next(
match self.pl.poll() { self: Pin<&mut Self>,
Ok(Async::Ready(Some(chunk))) => { cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
match Pin::new(&mut this.pl).poll_data(cx) {
Poll::Ready(Some(Ok(chunk))) => {
let len = chunk.len(); let len = chunk.len();
if let Err(err) = self.pl.release_capacity().release_capacity(len) { if let Err(err) = this.pl.flow_control().release_capacity(len) {
Err(err.into()) Poll::Ready(Some(Err(err.into())))
} else { } else {
Ok(Async::Ready(Some(chunk))) Poll::Ready(Some(Ok(chunk)))
} }
} }
Ok(Async::Ready(None)) => Ok(Async::Ready(None)), Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err.into()))),
Ok(Async::NotReady) => Ok(Async::NotReady), Poll::Pending => Poll::Pending,
Err(err) => Err(err.into()), Poll::Ready(None) => Poll::Ready(None),
} }
} }
} }

View File

@ -1,192 +1,343 @@
use std::fmt::Debug; use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::{io, net}; use std::pin::Pin;
use std::task::{Context, Poll};
use std::{net, rc};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_server_config::{Io, ServerConfig as SrvConfig}; use actix_rt::net::TcpStream;
use actix_service::{IntoNewService, NewService, Service}; use actix_service::{
use actix_utils::cloneable::CloneableService; fn_factory, fn_service, pipeline_factory, IntoServiceFactory, Service,
ServiceFactory,
};
use bytes::Bytes; use bytes::Bytes;
use futures::future::{ok, FutureResult}; use futures_core::ready;
use futures::{try_ready, Async, Future, IntoFuture, Poll, Stream}; use futures_util::future::ok;
use h2::server::{self, Connection, Handshake}; use h2::server::{self, Handshake};
use h2::RecvStream;
use log::error; use log::error;
use crate::body::MessageBody; use crate::body::MessageBody;
use crate::config::{KeepAlive, ServiceConfig}; use crate::cloneable::CloneableService;
use crate::error::{DispatchError, Error, ParseError, ResponseError}; use crate::config::ServiceConfig;
use crate::payload::Payload; use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use super::dispatcher::Dispatcher; use super::dispatcher::Dispatcher;
/// `NewService` implementation for HTTP2 transport /// `ServiceFactory` implementation for HTTP2 transport
pub struct H2Service<T, P, S, B> { pub struct H2Service<T, S, B> {
srv: S, srv: S,
cfg: ServiceConfig, cfg: ServiceConfig,
_t: PhantomData<(T, P, B)>, on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
_t: PhantomData<(T, B)>,
} }
impl<T, P, S, B> H2Service<T, P, S, B> impl<T, S, B> H2Service<T, S, B>
where where
S: NewService<SrvConfig, Request = Request>, S: ServiceFactory<Config = (), Request = Request>,
S::Service: 'static, S::Error: Into<Error> + 'static,
S::Error: Debug + 'static, S::Response: Into<Response<B>> + 'static,
S::Response: Into<Response<B>>, <S::Service as Service>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
/// Create new `HttpService` instance.
pub fn new<F: IntoNewService<S, SrvConfig>>(service: F) -> Self {
let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0);
H2Service {
cfg,
srv: service.into_new_service(),
_t: PhantomData,
}
}
/// Create new `HttpService` instance with config. /// Create new `HttpService` instance with config.
pub fn with_config<F: IntoNewService<S, SrvConfig>>( pub(crate) fn with_config<F: IntoServiceFactory<S>>(
cfg: ServiceConfig, cfg: ServiceConfig,
service: F, service: F,
) -> Self { ) -> Self {
H2Service { H2Service {
cfg, cfg,
srv: service.into_new_service(), on_connect: None,
srv: service.into_factory(),
_t: PhantomData, _t: PhantomData,
} }
} }
/// Set on connect callback.
pub(crate) fn on_connect(
mut self,
f: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self {
self.on_connect = f;
self
}
}
impl<S, B> H2Service<TcpStream, S, B>
where
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
/// Create simple tcp based service
pub fn tcp(
self,
) -> impl ServiceFactory<
Config = (),
Request = TcpStream,
Response = (),
Error = DispatchError,
InitError = S::InitError,
> {
pipeline_factory(fn_factory(|| {
async {
Ok::<_, S::InitError>(fn_service(|io: TcpStream| {
let peer_addr = io.peer_addr().ok();
ok::<_, DispatchError>((io, peer_addr))
}))
}
}))
.and_then(self)
}
}
#[cfg(feature = "openssl")]
mod openssl {
use actix_service::{fn_factory, fn_service};
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
use actix_tls::{openssl::HandshakeError, SslError};
use super::*;
impl<S, B> H2Service<SslStream<TcpStream>, S, B>
where
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
/// Create ssl based service
pub fn openssl(
self,
acceptor: SslAcceptor,
) -> impl ServiceFactory<
Config = (),
Request = TcpStream,
Response = (),
Error = SslError<HandshakeError<TcpStream>, DispatchError>,
InitError = S::InitError,
> {
pipeline_factory(
Acceptor::new(acceptor)
.map_err(SslError::Ssl)
.map_init_err(|_| panic!()),
)
.and_then(fn_factory(|| {
ok::<_, S::InitError>(fn_service(|io: SslStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok();
ok((io, peer_addr))
}))
}))
.and_then(self.map_err(SslError::Service))
}
}
} }
impl<T, P, S, B> NewService<SrvConfig> for H2Service<T, P, S, B> #[cfg(feature = "rustls")]
mod rustls {
use super::*;
use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::SslError;
use std::io;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
/// Create openssl based service
pub fn rustls(
self,
mut config: ServerConfig,
) -> impl ServiceFactory<
Config = (),
Request = TcpStream,
Response = (),
Error = SslError<io::Error, DispatchError>,
InitError = S::InitError,
> {
let protos = vec!["h2".to_string().into()];
config.set_protocols(&protos);
pipeline_factory(
Acceptor::new(config)
.map_err(SslError::Ssl)
.map_init_err(|_| panic!()),
)
.and_then(fn_factory(|| {
ok::<_, S::InitError>(fn_service(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
ok((io, peer_addr))
}))
}))
.and_then(self.map_err(SslError::Service))
}
}
}
impl<T, S, B> ServiceFactory for H2Service<T, S, B>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
S: NewService<SrvConfig, Request = Request>, S: ServiceFactory<Config = (), Request = Request>,
S::Service: 'static, S::Error: Into<Error> + 'static,
S::Error: Debug, S::Response: Into<Response<B>> + 'static,
S::Response: Into<Response<B>>, <S::Service as Service>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
type Request = Io<T, P>; type Config = ();
type Request = (T, Option<net::SocketAddr>);
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type InitError = S::InitError; type InitError = S::InitError;
type Service = H2ServiceHandler<T, P, S::Service, B>; type Service = H2ServiceHandler<T, S::Service, B>;
type Future = H2ServiceResponse<T, P, S, B>; type Future = H2ServiceResponse<T, S, B>;
fn new_service(&self, cfg: &SrvConfig) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
H2ServiceResponse { H2ServiceResponse {
fut: self.srv.new_service(cfg).into_future(), fut: self.srv.new_service(()),
cfg: Some(self.cfg.clone()), cfg: Some(self.cfg.clone()),
on_connect: self.on_connect.clone(),
_t: PhantomData, _t: PhantomData,
} }
} }
} }
#[doc(hidden)] #[doc(hidden)]
pub struct H2ServiceResponse<T, P, S: NewService<SrvConfig, Request = Request>, B> { #[pin_project::pin_project]
fut: <S::Future as IntoFuture>::Future, pub struct H2ServiceResponse<T, S: ServiceFactory, B> {
#[pin]
fut: S::Future,
cfg: Option<ServiceConfig>, cfg: Option<ServiceConfig>,
_t: PhantomData<(T, P, B)>, on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
_t: PhantomData<(T, B)>,
} }
impl<T, P, S, B> Future for H2ServiceResponse<T, P, S, B> impl<T, S, B> Future for H2ServiceResponse<T, S, B>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
S: NewService<SrvConfig, Request = Request>, S: ServiceFactory<Config = (), Request = Request>,
S::Service: 'static, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>> + 'static,
S::Error: Debug, <S::Service as Service>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
type Item = H2ServiceHandler<T, P, S::Service, B>; type Output = Result<H2ServiceHandler<T, S::Service, B>, S::InitError>;
type Error = S::InitError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let service = try_ready!(self.fut.poll()); let this = self.as_mut().project();
Ok(Async::Ready(H2ServiceHandler::new(
self.cfg.take().unwrap(), Poll::Ready(ready!(this.fut.poll(cx)).map(|service| {
service, let this = self.as_mut().project();
))) H2ServiceHandler::new(
this.cfg.take().unwrap(),
this.on_connect.clone(),
service,
)
}))
} }
} }
/// `Service` implementation for http/2 transport /// `Service` implementation for http/2 transport
pub struct H2ServiceHandler<T, P, S: 'static, B> { pub struct H2ServiceHandler<T, S, B> {
srv: CloneableService<S>, srv: CloneableService<S>,
cfg: ServiceConfig, cfg: ServiceConfig,
_t: PhantomData<(T, P, B)>, on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
_t: PhantomData<(T, B)>,
} }
impl<T, P, S, B> H2ServiceHandler<T, P, S, B> impl<T, S, B> H2ServiceHandler<T, S, B>
where where
S: Service<Request = Request> + 'static, S: Service<Request = Request>,
S::Error: Debug, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>>, S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
fn new(cfg: ServiceConfig, srv: S) -> H2ServiceHandler<T, P, S, B> { fn new(
cfg: ServiceConfig,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
srv: S,
) -> H2ServiceHandler<T, S, B> {
H2ServiceHandler { H2ServiceHandler {
cfg, cfg,
on_connect,
srv: CloneableService::new(srv), srv: CloneableService::new(srv),
_t: PhantomData, _t: PhantomData,
} }
} }
} }
impl<T, P, S, B> Service for H2ServiceHandler<T, P, S, B> impl<T, S, B> Service for H2ServiceHandler<T, S, B>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request> + 'static, S: Service<Request = Request>,
S::Error: Debug, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>>, S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
type Request = Io<T, P>; type Request = (T, Option<net::SocketAddr>);
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) -> Poll<(), Self::Error> { fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.srv.poll_ready().map_err(|e| { self.srv.poll_ready(cx).map_err(|e| {
let e = e.into();
error!("Service readiness error: {:?}", e); error!("Service readiness error: {:?}", e);
DispatchError::Service DispatchError::Service(e)
}) })
} }
fn call(&mut self, req: Self::Request) -> Self::Future { fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let on_connect = if let Some(ref on_connect) = self.on_connect {
Some(on_connect(&io))
} else {
None
};
H2ServiceHandlerResponse { H2ServiceHandlerResponse {
state: State::Handshake( state: State::Handshake(
Some(self.srv.clone()), Some(self.srv.clone()),
Some(self.cfg.clone()), Some(self.cfg.clone()),
server::handshake(req.into_parts().0), addr,
on_connect,
server::handshake(io),
), ),
} }
} }
} }
enum State< enum State<T, S: Service<Request = Request>, B: MessageBody>
T: AsyncRead + AsyncWrite, where
S: Service<Request = Request> + 'static, T: AsyncRead + AsyncWrite + Unpin,
B: MessageBody, S::Future: 'static,
> { {
Incoming(Dispatcher<T, S, B>), Incoming(Dispatcher<T, S, B>),
Handshake( Handshake(
Option<CloneableService<S>>, Option<CloneableService<S>>,
Option<ServiceConfig>, Option<ServiceConfig>,
Option<net::SocketAddr>,
Option<Box<dyn DataFactory>>,
Handshake<T, Bytes>, Handshake<T, Bytes>,
), ),
} }
pub struct H2ServiceHandlerResponse<T, S, B> pub struct H2ServiceHandlerResponse<T, S, B>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request> + 'static, S: Service<Request = Request>,
S::Error: Debug, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>>, S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
state: State<T, S, B>, state: State<T, S, B>,
@ -194,36 +345,42 @@ where
impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B> impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request> + 'static, S: Service<Request = Request>,
S::Error: Debug, S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>>, S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody, B: MessageBody,
{ {
type Item = (); type Output = Result<(), DispatchError>;
type Error = DispatchError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.state { match self.state {
State::Incoming(ref mut disp) => disp.poll(), State::Incoming(ref mut disp) => Pin::new(disp).poll(cx),
State::Handshake(ref mut srv, ref mut config, ref mut handshake) => { State::Handshake(
match handshake.poll() { ref mut srv,
Ok(Async::Ready(conn)) => { ref mut config,
self.state = State::Incoming(Dispatcher::new( ref peer_addr,
srv.take().unwrap(), ref mut on_connect,
conn, ref mut handshake,
config.take().unwrap(), ) => match Pin::new(handshake).poll(cx) {
None, Poll::Ready(Ok(conn)) => {
)); self.state = State::Incoming(Dispatcher::new(
self.poll() srv.take().unwrap(),
} conn,
Ok(Async::NotReady) => Ok(Async::NotReady), on_connect.take(),
Err(err) => { config.take().unwrap(),
trace!("H2 handshake error: {}", err); None,
return Err(err.into()); *peer_addr,
} ));
self.poll(cx)
} }
} Poll::Ready(Err(err)) => {
trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err.into()))
}
Poll::Pending => Poll::Pending,
},
} }
} }
} }

View File

@ -64,7 +64,7 @@ impl Header for CacheControl {
where where
T: crate::HttpMessage, T: crate::HttpMessage,
{ {
let directives = from_comma_delimited(msg.headers().get_all(Self::name()))?; let directives = from_comma_delimited(msg.headers().get_all(&Self::name()))?;
if !directives.is_empty() { if !directives.is_empty() {
Ok(CacheControl(directives)) Ok(CacheControl(directives))
} else { } else {
@ -74,18 +74,18 @@ impl Header for CacheControl {
} }
impl fmt::Display for CacheControl { impl fmt::Display for CacheControl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt_comma_delimited(f, &self[..]) fmt_comma_delimited(f, &self[..])
} }
} }
impl IntoHeaderValue for CacheControl { impl IntoHeaderValue for CacheControl {
type Error = header::InvalidHeaderValueBytes; type Error = header::InvalidHeaderValue;
fn try_into(self) -> Result<header::HeaderValue, Self::Error> { fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
let mut writer = Writer::new(); let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_shared(writer.take()) header::HeaderValue::from_maybe_shared(writer.take())
} }
} }
@ -126,7 +126,7 @@ pub enum CacheDirective {
} }
impl fmt::Display for CacheDirective { impl fmt::Display for CacheDirective {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::CacheDirective::*; use self::CacheDirective::*;
fmt::Display::fmt( fmt::Display::fmt(
match *self { match *self {

View File

@ -70,11 +70,17 @@ impl<'a> From<&'a str> for DispositionType {
/// assert_eq!(param.as_filename().unwrap(), "sample.txt"); /// assert_eq!(param.as_filename().unwrap(), "sample.txt");
/// ``` /// ```
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum DispositionParam { pub enum DispositionParam {
/// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from
/// the form. /// the form.
Name(String), Name(String),
/// A plain file name. /// A plain file name.
///
/// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any
/// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where
/// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead
/// in case there are Unicode characters in file names.
Filename(String), Filename(String),
/// An extended file name. It must not exist for `ContentType::Formdata` according to /// An extended file name. It must not exist for `ContentType::Formdata` according to
/// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2).
@ -219,7 +225,16 @@ impl DispositionParam {
/// ext-token = <the characters in token, followed by "*"> /// ext-token = <the characters in token, followed by "*">
/// ``` /// ```
/// ///
/// **Note**: filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within /// # Note
///
/// filename is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any
/// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where
/// filename* with charset UTF-8 may be used instead in case there are Unicode characters in file
/// names.
/// filename is [acceptable](https://tools.ietf.org/html/rfc7578#section-4.2) to be UTF-8 encoded
/// directly in a *Content-Disposition* header for *multipart/form-data*, though.
///
/// filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within
/// *multipart/form-data*. /// *multipart/form-data*.
/// ///
/// # Example /// # Example
@ -250,6 +265,22 @@ impl DispositionParam {
/// }; /// };
/// assert_eq!(cd2.get_name(), Some("file")); // field name /// assert_eq!(cd2.get_name(), Some("file")); // field name
/// assert_eq!(cd2.get_filename(), Some("bill.odt")); /// assert_eq!(cd2.get_filename(), Some("bill.odt"));
///
/// // HTTP response header with Unicode characters in file names
/// let cd3 = ContentDisposition {
/// disposition: DispositionType::Attachment,
/// parameters: vec![
/// DispositionParam::FilenameExt(ExtendedValue {
/// charset: Charset::Ext(String::from("UTF-8")),
/// language_tag: None,
/// value: String::from("\u{1f600}.svg").into_bytes(),
/// }),
/// // fallback for better compatibility
/// DispositionParam::Filename(String::from("Grinning-Face-Emoji.svg"))
/// ],
/// };
/// assert_eq!(cd3.get_filename_ext().map(|ev| ev.value.as_ref()),
/// Some("\u{1f600}.svg".as_bytes()));
/// ``` /// ```
/// ///
/// # WARN /// # WARN
@ -332,15 +363,17 @@ impl ContentDisposition {
// token: won't contains semicolon according to RFC 2616 Section 2.2 // token: won't contains semicolon according to RFC 2616 Section 2.2
let (token, new_left) = split_once_and_trim(left, ';'); let (token, new_left) = split_once_and_trim(left, ';');
left = new_left; left = new_left;
if token.is_empty() {
// quoted-string can be empty, but token cannot be empty
return Err(crate::error::ParseError::Header);
}
token.to_owned() token.to_owned()
}; };
if value.is_empty() {
return Err(crate::error::ParseError::Header);
}
let param = if param_name.eq_ignore_ascii_case("name") { let param = if param_name.eq_ignore_ascii_case("name") {
DispositionParam::Name(value) DispositionParam::Name(value)
} else if param_name.eq_ignore_ascii_case("filename") { } else if param_name.eq_ignore_ascii_case("filename") {
// See also comments in test_from_raw_uncessary_percent_decode.
DispositionParam::Filename(value) DispositionParam::Filename(value)
} else { } else {
DispositionParam::Unknown(param_name.to_owned(), value) DispositionParam::Unknown(param_name.to_owned(), value)
@ -429,12 +462,12 @@ impl ContentDisposition {
} }
impl IntoHeaderValue for ContentDisposition { impl IntoHeaderValue for ContentDisposition {
type Error = header::InvalidHeaderValueBytes; type Error = header::InvalidHeaderValue;
fn try_into(self) -> Result<header::HeaderValue, Self::Error> { fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
let mut writer = Writer::new(); let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_shared(writer.take()) header::HeaderValue::from_maybe_shared(writer.take())
} }
} }
@ -444,7 +477,7 @@ impl Header for ContentDisposition {
} }
fn parse<T: crate::HttpMessage>(msg: &T) -> Result<Self, crate::error::ParseError> { fn parse<T: crate::HttpMessage>(msg: &T) -> Result<Self, crate::error::ParseError> {
if let Some(h) = msg.headers().get(Self::name()) { if let Some(h) = msg.headers().get(&Self::name()) {
Self::from_raw(&h) Self::from_raw(&h)
} else { } else {
Err(crate::error::ParseError::Header) Err(crate::error::ParseError::Header)
@ -453,7 +486,7 @@ impl Header for ContentDisposition {
} }
impl fmt::Display for DispositionType { impl fmt::Display for DispositionType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
DispositionType::Inline => write!(f, "inline"), DispositionType::Inline => write!(f, "inline"),
DispositionType::Attachment => write!(f, "attachment"), DispositionType::Attachment => write!(f, "attachment"),
@ -464,12 +497,41 @@ impl fmt::Display for DispositionType {
} }
impl fmt::Display for DispositionParam { impl fmt::Display for DispositionParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// All ASCII control charaters (0-30, 127) excepting horizontal tab, double quote, and // All ASCII control characters (0-30, 127) including horizontal tab, double quote, and
// backslash should be escaped in quoted-string (i.e. "foobar"). // backslash should be escaped in quoted-string (i.e. "foobar").
// Ref: RFC6266 S4.1 -> RFC2616 S2.2; RFC 7578 S4.2 -> RFC2183 S2 -> ... . // Ref: RFC6266 S4.1 -> RFC2616 S3.6
// filename-parm = "filename" "=" value
// value = token | quoted-string
// quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
// qdtext = <any TEXT except <">>
// quoted-pair = "\" CHAR
// TEXT = <any OCTET except CTLs,
// but including LWS>
// LWS = [CRLF] 1*( SP | HT )
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
//
// Ref: RFC7578 S4.2 -> RFC2183 S2 -> RFC2045 S5.1
// parameter := attribute "=" value
// attribute := token
// ; Matching of attributes
// ; is ALWAYS case-insensitive.
// value := token / quoted-string
// token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
// or tspecials>
// tspecials := "(" / ")" / "<" / ">" / "@" /
// "," / ";" / ":" / "\" / <">
// "/" / "[" / "]" / "?" / "="
// ; Must be in quoted-string,
// ; to use within parameter values
//
//
// See also comments in test_from_raw_uncessary_percent_decode.
lazy_static! { lazy_static! {
static ref RE: Regex = Regex::new("[\x01-\x08\x10\x1F\x7F\"\\\\]").unwrap(); static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap();
} }
match self { match self {
DispositionParam::Name(ref value) => write!(f, "name={}", value), DispositionParam::Name(ref value) => write!(f, "name={}", value),
@ -493,7 +555,7 @@ impl fmt::Display for DispositionParam {
} }
impl fmt::Display for ContentDisposition { impl fmt::Display for ContentDisposition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.disposition)?; write!(f, "{}", self.disposition)?;
self.parameters self.parameters
.iter() .iter()
@ -706,9 +768,8 @@ mod tests {
Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above. Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above.
(And now, only UTF-8 is handled by this implementation.) (And now, only UTF-8 is handled by this implementation.)
*/ */
let a = let a = HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"")
HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"") .unwrap();
.unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
disposition: DispositionType::FormData, disposition: DispositionType::FormData,
@ -719,8 +780,10 @@ mod tests {
}; };
assert_eq!(a, b); assert_eq!(a, b);
let a = let a = HeaderValue::from_str(
HeaderValue::from_str("form-data; name=upload; filename=\"余固知謇謇之為患兮,忍而不能舍也.pptx\"").unwrap(); "form-data; name=upload; filename=\"余固知謇謇之為患兮,忍而不能舍也.pptx\"",
)
.unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
disposition: DispositionType::FormData, disposition: DispositionType::FormData,
@ -771,8 +834,18 @@ mod tests {
#[test] #[test]
fn test_from_raw_uncessary_percent_decode() { fn test_from_raw_uncessary_percent_decode() {
// In fact, RFC7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with
// non-ASCII characters MAY be percent-encoded.
// On the contrary, RFC6266 or other RFCs related to Content-Disposition response header
// do not mention such percent-encoding.
// So, it appears to be undecidable whether to percent-decode or not without
// knowing the usage scenario (multipart/form-data v.s. HTTP response header) and
// inevitable to unnecessarily percent-decode filename with %XX in the former scenario.
// Fortunately, it seems that almost all mainstream browsers just send UTF-8 encoded file
// names in quoted-string format (tested on Edge, IE11, Chrome and Firefox) without
// percent-encoding. So we do not bother to attempt to percent-decode.
let a = HeaderValue::from_static( let a = HeaderValue::from_static(
"form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"", // Should not be decoded! "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"",
); );
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
@ -808,6 +881,13 @@ mod tests {
let a = HeaderValue::from_static("inline; filename= "); let a = HeaderValue::from_static("inline; filename= ");
assert!(ContentDisposition::from_raw(&a).is_err()); assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; filename=\"\"");
assert!(ContentDisposition::from_raw(&a)
.expect("parse cd")
.get_filename()
.expect("filename")
.is_empty());
} }
#[test] #[test]

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use crate::error::ParseError; use crate::error::ParseError;
use crate::header::{ use crate::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer, CONTENT_RANGE, HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE,
}; };
header! { header! {
@ -166,7 +166,7 @@ impl FromStr for ContentRangeSpec {
} }
impl Display for ContentRangeSpec { impl Display for ContentRangeSpec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self { match *self {
ContentRangeSpec::Bytes { ContentRangeSpec::Bytes {
range, range,
@ -198,11 +198,11 @@ impl Display for ContentRangeSpec {
} }
impl IntoHeaderValue for ContentRangeSpec { impl IntoHeaderValue for ContentRangeSpec {
type Error = InvalidHeaderValueBytes; type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new(); let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
HeaderValue::from_shared(writer.take()) HeaderValue::from_maybe_shared(writer.take())
} }
} }

View File

@ -3,7 +3,7 @@ use std::fmt::{self, Display, Write};
use crate::error::ParseError; use crate::error::ParseError;
use crate::header::{ use crate::header::{
self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate,
IntoHeaderValue, InvalidHeaderValueBytes, Writer, IntoHeaderValue, InvalidHeaderValue, Writer,
}; };
use crate::httpmessage::HttpMessage; use crate::httpmessage::HttpMessage;
@ -73,12 +73,12 @@ impl Header for IfRange {
T: HttpMessage, T: HttpMessage,
{ {
let etag: Result<EntityTag, _> = let etag: Result<EntityTag, _> =
from_one_raw_str(msg.headers().get(header::IF_RANGE)); from_one_raw_str(msg.headers().get(&header::IF_RANGE));
if let Ok(etag) = etag { if let Ok(etag) = etag {
return Ok(IfRange::EntityTag(etag)); return Ok(IfRange::EntityTag(etag));
} }
let date: Result<HttpDate, _> = let date: Result<HttpDate, _> =
from_one_raw_str(msg.headers().get(header::IF_RANGE)); from_one_raw_str(msg.headers().get(&header::IF_RANGE));
if let Ok(date) = date { if let Ok(date) = date {
return Ok(IfRange::Date(date)); return Ok(IfRange::Date(date));
} }
@ -87,7 +87,7 @@ impl Header for IfRange {
} }
impl Display for IfRange { impl Display for IfRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self { match *self {
IfRange::EntityTag(ref x) => Display::fmt(x, f), IfRange::EntityTag(ref x) => Display::fmt(x, f),
IfRange::Date(ref x) => Display::fmt(x, f), IfRange::Date(ref x) => Display::fmt(x, f),
@ -96,12 +96,12 @@ impl Display for IfRange {
} }
impl IntoHeaderValue for IfRange { impl IntoHeaderValue for IfRange {
type Error = InvalidHeaderValueBytes; type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new(); let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
HeaderValue::from_shared(writer.take()) HeaderValue::from_maybe_shared(writer.take())
} }
} }

View File

@ -159,18 +159,18 @@ macro_rules! header {
} }
impl std::fmt::Display for $id { impl std::fmt::Display for $id {
#[inline] #[inline]
fn fmt(&self, f: &mut std::fmt::Formatter) -> ::std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result {
$crate::http::header::fmt_comma_delimited(f, &self.0[..]) $crate::http::header::fmt_comma_delimited(f, &self.0[..])
} }
} }
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValueBytes; type Error = $crate::http::header::InvalidHeaderValue;
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use std::fmt::Write; use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new(); let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
$crate::http::header::HeaderValue::from_shared(writer.take()) $crate::http::header::HeaderValue::from_maybe_shared(writer.take())
} }
} }
}; };
@ -195,18 +195,18 @@ macro_rules! header {
} }
impl std::fmt::Display for $id { impl std::fmt::Display for $id {
#[inline] #[inline]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
$crate::http::header::fmt_comma_delimited(f, &self.0[..]) $crate::http::header::fmt_comma_delimited(f, &self.0[..])
} }
} }
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValueBytes; type Error = $crate::http::header::InvalidHeaderValue;
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use std::fmt::Write; use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new(); let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
$crate::http::header::HeaderValue::from_shared(writer.take()) $crate::http::header::HeaderValue::from_maybe_shared(writer.take())
} }
} }
}; };
@ -231,12 +231,12 @@ macro_rules! header {
} }
impl std::fmt::Display for $id { impl std::fmt::Display for $id {
#[inline] #[inline]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f) std::fmt::Display::fmt(&self.0, f)
} }
} }
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValueBytes; type Error = $crate::http::header::InvalidHeaderValue;
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
self.0.try_into() self.0.try_into()
@ -276,7 +276,7 @@ macro_rules! header {
} }
impl std::fmt::Display for $id { impl std::fmt::Display for $id {
#[inline] #[inline]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self { match *self {
$id::Any => f.write_str("*"), $id::Any => f.write_str("*"),
$id::Items(ref fields) => $crate::http::header::fmt_comma_delimited( $id::Items(ref fields) => $crate::http::header::fmt_comma_delimited(
@ -285,13 +285,13 @@ macro_rules! header {
} }
} }
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValueBytes; type Error = $crate::http::header::InvalidHeaderValue;
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use std::fmt::Write; use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new(); let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
$crate::http::header::HeaderValue::from_shared(writer.take()) $crate::http::header::HeaderValue::from_maybe_shared(writer.take())
} }
} }
}; };

View File

@ -0,0 +1,385 @@
use std::collections::hash_map::{self, Entry};
use std::convert::TryFrom;
use either::Either;
use fxhash::FxHashMap;
use http::header::{HeaderName, HeaderValue};
/// A set of HTTP headers
///
/// `HeaderMap` is an multimap of [`HeaderName`] to values.
///
/// [`HeaderName`]: struct.HeaderName.html
#[derive(Debug, Clone)]
pub struct HeaderMap {
pub(crate) inner: FxHashMap<HeaderName, Value>,
}
#[derive(Debug, Clone)]
pub(crate) enum Value {
One(HeaderValue),
Multi(Vec<HeaderValue>),
}
impl Value {
fn get(&self) -> &HeaderValue {
match self {
Value::One(ref val) => val,
Value::Multi(ref val) => &val[0],
}
}
fn get_mut(&mut self) -> &mut HeaderValue {
match self {
Value::One(ref mut val) => val,
Value::Multi(ref mut val) => &mut val[0],
}
}
fn append(&mut self, val: HeaderValue) {
match self {
Value::One(_) => {
let data = std::mem::replace(self, Value::Multi(vec![val]));
match data {
Value::One(val) => self.append(val),
Value::Multi(_) => unreachable!(),
}
}
Value::Multi(ref mut vec) => vec.push(val),
}
}
}
impl HeaderMap {
/// Create an empty `HeaderMap`.
///
/// The map will be created without any capacity. This function will not
/// allocate.
pub fn new() -> Self {
HeaderMap {
inner: FxHashMap::default(),
}
}
/// Create an empty `HeaderMap` with the specified capacity.
///
/// The returned map will allocate internal storage in order to hold about
/// `capacity` elements without reallocating. However, this is a "best
/// effort" as there are usage patterns that could cause additional
/// allocations before `capacity` headers are stored in the map.
///
/// More capacity than requested may be allocated.
pub fn with_capacity(capacity: usize) -> HeaderMap {
HeaderMap {
inner: FxHashMap::with_capacity_and_hasher(capacity, Default::default()),
}
}
/// Returns the number of keys stored in the map.
///
/// This number could be be less than or equal to actual headers stored in
/// the map.
pub fn len(&self) -> usize {
self.inner.len()
}
/// Returns true if the map contains no elements.
pub fn is_empty(&self) -> bool {
self.inner.len() == 0
}
/// Clears the map, removing all key-value pairs. Keeps the allocated memory
/// for reuse.
pub fn clear(&mut self) {
self.inner.clear();
}
/// Returns the number of headers the map can hold without reallocating.
///
/// This number is an approximation as certain usage patterns could cause
/// additional allocations before the returned capacity is filled.
pub fn capacity(&self) -> usize {
self.inner.capacity()
}
/// Reserves capacity for at least `additional` more headers to be inserted
/// into the `HeaderMap`.
///
/// The header map may reserve more space to avoid frequent reallocations.
/// Like with `with_capacity`, this will be a "best effort" to avoid
/// allocations until `additional` more headers are inserted. Certain usage
/// patterns could cause additional allocations before the number is
/// reached.
pub fn reserve(&mut self, additional: usize) {
self.inner.reserve(additional)
}
/// Returns a reference to the value associated with the key.
///
/// If there are multiple values associated with the key, then the first one
/// is returned. Use `get_all` to get all values associated with a given
/// key. Returns `None` if there are no values associated with the key.
pub fn get<N: AsName>(&self, name: N) -> Option<&HeaderValue> {
self.get2(name).map(|v| v.get())
}
fn get2<N: AsName>(&self, name: N) -> Option<&Value> {
match name.as_name() {
Either::Left(name) => self.inner.get(name),
Either::Right(s) => {
if let Ok(name) = HeaderName::try_from(s) {
self.inner.get(&name)
} else {
None
}
}
}
}
/// Returns a view of all values associated with a key.
///
/// The returned view does not incur any allocations and allows iterating
/// the values associated with the key. See [`GetAll`] for more details.
/// Returns `None` if there are no values associated with the key.
///
/// [`GetAll`]: struct.GetAll.html
pub fn get_all<N: AsName>(&self, name: N) -> GetAll<'_> {
GetAll {
idx: 0,
item: self.get2(name),
}
}
/// Returns a mutable reference to the value associated with the key.
///
/// If there are multiple values associated with the key, then the first one
/// is returned. Use `entry` to get all values associated with a given
/// key. Returns `None` if there are no values associated with the key.
pub fn get_mut<N: AsName>(&mut self, name: N) -> Option<&mut HeaderValue> {
match name.as_name() {
Either::Left(name) => self.inner.get_mut(name).map(|v| v.get_mut()),
Either::Right(s) => {
if let Ok(name) = HeaderName::try_from(s) {
self.inner.get_mut(&name).map(|v| v.get_mut())
} else {
None
}
}
}
}
/// Returns true if the map contains a value for the specified key.
pub fn contains_key<N: AsName>(&self, key: N) -> bool {
match key.as_name() {
Either::Left(name) => self.inner.contains_key(name),
Either::Right(s) => {
if let Ok(name) = HeaderName::try_from(s) {
self.inner.contains_key(&name)
} else {
false
}
}
}
}
/// An iterator visiting all key-value pairs.
///
/// The iteration order is arbitrary, but consistent across platforms for
/// the same crate version. Each key will be yielded once per associated
/// value. So, if a key has 3 associated values, it will be yielded 3 times.
pub fn iter(&self) -> Iter<'_> {
Iter::new(self.inner.iter())
}
/// An iterator visiting all keys.
///
/// The iteration order is arbitrary, but consistent across platforms for
/// the same crate version. Each key will be yielded only once even if it
/// has multiple associated values.
pub fn keys(&self) -> Keys<'_> {
Keys(self.inner.keys())
}
/// Inserts a key-value pair into the map.
///
/// If the map did not previously have this key present, then `None` is
/// returned.
///
/// If the map did have this key present, the new value is associated with
/// the key and all previous values are removed. **Note** that only a single
/// one of the previous values is returned. If there are multiple values
/// that have been previously associated with the key, then the first one is
/// returned. See `insert_mult` on `OccupiedEntry` for an API that returns
/// all values.
///
/// The key is not updated, though; this matters for types that can be `==`
/// without being identical.
pub fn insert(&mut self, key: HeaderName, val: HeaderValue) {
let _ = self.inner.insert(key, Value::One(val));
}
/// Inserts a key-value pair into the map.
///
/// If the map did not previously have this key present, then `false` is
/// returned.
///
/// If the map did have this key present, the new value is pushed to the end
/// of the list of values currently associated with the key. The key is not
/// updated, though; this matters for types that can be `==` without being
/// identical.
pub fn append(&mut self, key: HeaderName, value: HeaderValue) {
match self.inner.entry(key) {
Entry::Occupied(mut entry) => entry.get_mut().append(value),
Entry::Vacant(entry) => {
entry.insert(Value::One(value));
}
}
}
/// Removes all headers for a particular header name from the map.
pub fn remove<N: AsName>(&mut self, key: N) {
match key.as_name() {
Either::Left(name) => {
let _ = self.inner.remove(name);
}
Either::Right(s) => {
if let Ok(name) = HeaderName::try_from(s) {
let _ = self.inner.remove(&name);
}
}
}
}
}
#[doc(hidden)]
pub trait AsName {
fn as_name(&self) -> Either<&HeaderName, &str>;
}
impl AsName for HeaderName {
fn as_name(&self) -> Either<&HeaderName, &str> {
Either::Left(self)
}
}
impl<'a> AsName for &'a HeaderName {
fn as_name(&self) -> Either<&HeaderName, &str> {
Either::Left(self)
}
}
impl<'a> AsName for &'a str {
fn as_name(&self) -> Either<&HeaderName, &str> {
Either::Right(self)
}
}
impl AsName for String {
fn as_name(&self) -> Either<&HeaderName, &str> {
Either::Right(self.as_str())
}
}
impl<'a> AsName for &'a String {
fn as_name(&self) -> Either<&HeaderName, &str> {
Either::Right(self.as_str())
}
}
pub struct GetAll<'a> {
idx: usize,
item: Option<&'a Value>,
}
impl<'a> Iterator for GetAll<'a> {
type Item = &'a HeaderValue;
#[inline]
fn next(&mut self) -> Option<&'a HeaderValue> {
if let Some(ref val) = self.item {
match val {
Value::One(ref val) => {
self.item.take();
Some(val)
}
Value::Multi(ref vec) => {
if self.idx < vec.len() {
let item = Some(&vec[self.idx]);
self.idx += 1;
item
} else {
self.item.take();
None
}
}
}
} else {
None
}
}
}
pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>);
impl<'a> Iterator for Keys<'a> {
type Item = &'a HeaderName;
#[inline]
fn next(&mut self) -> Option<&'a HeaderName> {
self.0.next()
}
}
impl<'a> IntoIterator for &'a HeaderMap {
type Item = (&'a HeaderName, &'a HeaderValue);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub struct Iter<'a> {
idx: usize,
current: Option<(&'a HeaderName, &'a Vec<HeaderValue>)>,
iter: hash_map::Iter<'a, HeaderName, Value>,
}
impl<'a> Iter<'a> {
fn new(iter: hash_map::Iter<'a, HeaderName, Value>) -> Self {
Self {
iter,
idx: 0,
current: None,
}
}
}
impl<'a> Iterator for Iter<'a> {
type Item = (&'a HeaderName, &'a HeaderValue);
#[inline]
fn next(&mut self) -> Option<(&'a HeaderName, &'a HeaderValue)> {
if let Some(ref mut item) = self.current {
if self.idx < item.1.len() {
let item = (item.0, &item.1[self.idx]);
self.idx += 1;
return Some(item);
} else {
self.idx = 0;
self.current.take();
}
}
if let Some(item) = self.iter.next() {
match item.1 {
Value::One(ref value) => Some((item.0, value)),
Value::Multi(ref vec) => {
self.current = Some((item.0, vec));
self.next()
}
}
} else {
None
}
}
}

View File

@ -1,12 +1,13 @@
//! Various http headers //! Various http headers
// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header) // 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 std::{fmt, str::FromStr};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::header::GetAll;
use http::Error as HttpError; use http::Error as HttpError;
use mime::Mime; use mime::Mime;
use percent_encoding::{AsciiSet, CONTROLS};
pub use http::header::*; pub use http::header::*;
@ -14,13 +15,16 @@ use crate::error::ParseError;
use crate::httpmessage::HttpMessage; use crate::httpmessage::HttpMessage;
mod common; mod common;
pub(crate) mod map;
mod shared; mod shared;
#[doc(hidden)]
pub use self::common::*; pub use self::common::*;
#[doc(hidden)] #[doc(hidden)]
pub use self::shared::*; pub use self::shared::*;
#[doc(hidden)] #[doc(hidden)]
pub use self::map::GetAll;
pub use self::map::HeaderMap;
/// A trait for any object that will represent a header field and value. /// A trait for any object that will represent a header field and value.
pub trait Header pub trait Header
where where
@ -33,7 +37,6 @@ where
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>; fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
} }
#[doc(hidden)]
/// A trait for any object that can be Converted to a `HeaderValue` /// A trait for any object that can be Converted to a `HeaderValue`
pub trait IntoHeaderValue: Sized { pub trait IntoHeaderValue: Sized {
/// The type returned in the event of a conversion error. /// The type returned in the event of a conversion error.
@ -71,38 +74,58 @@ impl<'a> IntoHeaderValue for &'a [u8] {
} }
impl IntoHeaderValue for Bytes { impl IntoHeaderValue for Bytes {
type Error = InvalidHeaderValueBytes; type Error = InvalidHeaderValue;
#[inline] #[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_shared(self) HeaderValue::from_maybe_shared(self)
} }
} }
impl IntoHeaderValue for Vec<u8> { impl IntoHeaderValue for Vec<u8> {
type Error = InvalidHeaderValueBytes; type Error = InvalidHeaderValue;
#[inline] #[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_shared(Bytes::from(self)) HeaderValue::try_from(self)
} }
} }
impl IntoHeaderValue for String { impl IntoHeaderValue for String {
type Error = InvalidHeaderValueBytes; type Error = InvalidHeaderValue;
#[inline] #[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_shared(Bytes::from(self)) 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 { impl IntoHeaderValue for Mime {
type Error = InvalidHeaderValueBytes; type Error = InvalidHeaderValue;
#[inline] #[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_shared(Bytes::from(format!("{}", self))) HeaderValue::try_from(format!("{}", self))
} }
} }
@ -182,7 +205,7 @@ impl Writer {
} }
} }
fn take(&mut self) -> Bytes { fn take(&mut self) -> Bytes {
self.buf.take().freeze() self.buf.split().freeze()
} }
} }
@ -194,7 +217,7 @@ impl fmt::Write for Writer {
} }
#[inline] #[inline]
fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
fmt::write(self, args) fmt::write(self, args)
} }
} }
@ -202,8 +225,8 @@ impl fmt::Write for Writer {
#[inline] #[inline]
#[doc(hidden)] #[doc(hidden)]
/// Reads a comma-delimited raw header into a Vec. /// Reads a comma-delimited raw header into a Vec.
pub fn from_comma_delimited<T: FromStr>( pub fn from_comma_delimited<'a, I: Iterator<Item = &'a HeaderValue> + 'a, T: FromStr>(
all: GetAll<HeaderValue>, all: I,
) -> Result<Vec<T>, ParseError> { ) -> Result<Vec<T>, ParseError> {
let mut result = Vec::new(); let mut result = Vec::new();
for h in all { for h in all {
@ -236,7 +259,7 @@ pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, Pars
#[inline] #[inline]
#[doc(hidden)] #[doc(hidden)]
/// Format an array into a comma-delimited string. /// Format an array into a comma-delimited string.
pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter<'_>, parts: &[T]) -> fmt::Result
where where
T: fmt::Display, T: fmt::Display,
{ {
@ -338,11 +361,9 @@ pub fn parse_extended_value(
} }
impl fmt::Display for ExtendedValue { impl fmt::Display for ExtendedValue {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let encoded_value = percent_encoding::percent_encode( let encoded_value =
&self.value[..], percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
self::percent_encoding_http::HTTP_VALUE,
);
if let Some(ref lang) = self.language_tag { if let Some(ref lang) = self.language_tag {
write!(f, "{}'{}'{}", self.charset, lang, encoded_value) write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
} else { } else {
@ -355,27 +376,46 @@ impl fmt::Display for ExtendedValue {
/// [https://tools.ietf.org/html/rfc5987#section-3.2][url] /// [https://tools.ietf.org/html/rfc5987#section-3.2][url]
/// ///
/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2 /// [url]: https://tools.ietf.org/html/rfc5987#section-3.2
pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result { pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
let encoded = let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE);
fmt::Display::fmt(&encoded, f) fmt::Display::fmt(&encoded, f)
} }
mod percent_encoding_http { /// Convert http::HeaderMap to a HeaderMap
use percent_encoding::{self, define_encode_set}; impl From<http::HeaderMap> for HeaderMap {
fn from(map: http::HeaderMap) -> HeaderMap {
// internal module because macro is hard-coded to make a public item let mut new_map = HeaderMap::with_capacity(map.capacity());
// but we don't want to public export this item for (h, v) in map.iter() {
define_encode_set! { new_map.append(h.clone(), v.clone());
// This encode set is used for HTTP header values and is defined at
// https://tools.ietf.org/html/rfc5987#section-3.2
pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | {
' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?',
'[', '\\', ']', '{', '}'
} }
new_map
} }
} }
// This encode set is used for HTTP header values and is defined at
// https://tools.ietf.org/html/rfc5987#section-3.2
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'%')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b',')
.add(b'/')
.add(b':')
.add(b';')
.add(b'<')
.add(b'-')
.add(b'>')
.add(b'?')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'{')
.add(b'}');
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::shared::Charset; use super::shared::Charset;

View File

@ -98,7 +98,7 @@ impl Charset {
} }
impl Display for Charset { impl Display for Charset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }

View File

@ -27,7 +27,7 @@ pub enum Encoding {
} }
impl fmt::Display for Encoding { impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match *self { f.write_str(match *self {
Chunked => "chunked", Chunked => "chunked",
Brotli => "br", Brotli => "br",

View File

@ -1,7 +1,7 @@
use std::fmt::{self, Display, Write}; use std::fmt::{self, Display, Write};
use std::str::FromStr; use std::str::FromStr;
use crate::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer}; use crate::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer};
/// check that each char in the slice is either: /// check that each char in the slice is either:
/// 1. `%x21`, or /// 1. `%x21`, or
@ -113,7 +113,7 @@ impl EntityTag {
} }
impl Display for EntityTag { impl Display for EntityTag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.weak { if self.weak {
write!(f, "W/\"{}\"", self.tag) write!(f, "W/\"{}\"", self.tag)
} else { } else {
@ -157,12 +157,12 @@ impl FromStr for EntityTag {
} }
impl IntoHeaderValue for EntityTag { impl IntoHeaderValue for EntityTag {
type Error = InvalidHeaderValueBytes; type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = Writer::new(); let mut wrt = Writer::new();
write!(wrt, "{}", self).unwrap(); write!(wrt, "{}", self).unwrap();
HeaderValue::from_shared(wrt.take()) HeaderValue::from_maybe_shared(wrt.take())
} }
} }

View File

@ -3,8 +3,8 @@ use std::io::Write;
use std::str::FromStr; use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use bytes::{BufMut, BytesMut}; use bytes::{buf::BufMutExt, BytesMut};
use http::header::{HeaderValue, InvalidHeaderValueBytes}; use http::header::{HeaderValue, InvalidHeaderValue};
use crate::error::ParseError; use crate::error::ParseError;
use crate::header::IntoHeaderValue; use crate::header::IntoHeaderValue;
@ -28,7 +28,7 @@ impl FromStr for HttpDate {
} }
impl Display for HttpDate { impl 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.to_utc().rfc822(), f) fmt::Display::fmt(&self.0.to_utc().rfc822(), f)
} }
} }
@ -58,12 +58,12 @@ impl From<SystemTime> for HttpDate {
} }
impl IntoHeaderValue for HttpDate { impl IntoHeaderValue for HttpDate {
type Error = InvalidHeaderValueBytes; type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = BytesMut::with_capacity(29).writer(); let mut wrt = BytesMut::with_capacity(29).writer();
write!(wrt, "{}", self.0.rfc822()).unwrap(); write!(wrt, "{}", self.0.rfc822()).unwrap();
HeaderValue::from_shared(wrt.get_mut().take().freeze()) HeaderValue::from_maybe_shared(wrt.get_mut().split().freeze())
} }
} }

View File

@ -53,7 +53,7 @@ impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> {
} }
impl<T: fmt::Display> fmt::Display for QualityItem<T> { impl<T: fmt::Display> fmt::Display for QualityItem<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.item, f)?; fmt::Display::fmt(&self.item, f)?;
match self.quality.0 { match self.quality.0 {
1000 => Ok(()), 1000 => Ok(()),

View File

@ -1,6 +1,9 @@
use std::{io, mem, ptr, slice};
use bytes::{BufMut, BytesMut}; use bytes::{BufMut, BytesMut};
use http::Version; use http::Version;
use std::{mem, ptr, slice};
use crate::extensions::Extensions;
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
2021222324252627282930313233343536373839\ 2021222324252627282930313233343536373839\
@ -57,7 +60,7 @@ pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesM
bytes.put_slice(&buf); bytes.put_slice(&buf);
if four { if four {
bytes.put(b' '); bytes.put_u8(b' ');
} }
} }
@ -112,7 +115,7 @@ pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) { pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) {
let mut curr: isize = 39; let mut curr: isize = 39;
let mut buf: [u8; 41] = unsafe { mem::uninitialized() }; let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() };
buf[39] = b'\r'; buf[39] = b'\r';
buf[40] = b'\n'; buf[40] = b'\n';
let buf_ptr = buf.as_mut_ptr(); let buf_ptr = buf.as_mut_ptr();
@ -167,6 +170,30 @@ pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) {
} }
} }
pub(crate) 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(())
}
}
pub(crate) trait DataFactory {
fn set(&self, ext: &mut Extensions);
}
pub(crate) struct Data<T>(pub(crate) T);
impl<T: Clone + 'static> DataFactory for Data<T> {
fn set(&self, ext: &mut Extensions) {
ext.insert(self.0.clone())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -176,33 +203,33 @@ mod tests {
let mut bytes = BytesMut::new(); let mut bytes = BytesMut::new();
bytes.reserve(50); bytes.reserve(50);
write_content_length(0, &mut bytes); write_content_length(0, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 0\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 0\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(9, &mut bytes); write_content_length(9, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 9\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(10, &mut bytes); write_content_length(10, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 10\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(99, &mut bytes); write_content_length(99, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 99\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(100, &mut bytes); write_content_length(100, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 100\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 100\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(101, &mut bytes); write_content_length(101, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 101\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 101\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(998, &mut bytes); write_content_length(998, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 998\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 998\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(1000, &mut bytes); write_content_length(1000, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 1000\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1000\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(1001, &mut bytes); write_content_length(1001, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 1001\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 1001\r\n"[..]);
bytes.reserve(50); bytes.reserve(50);
write_content_length(5909, &mut bytes); write_content_length(5909, &mut bytes);
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 5909\r\n"[..]); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 5909\r\n"[..]);
} }
} }

View File

@ -8,7 +8,7 @@ macro_rules! STATIC_RESP {
($name:ident, $status:expr) => { ($name:ident, $status:expr) => {
#[allow(non_snake_case, missing_docs)] #[allow(non_snake_case, missing_docs)]
pub fn $name() -> ResponseBuilder { pub fn $name() -> ResponseBuilder {
Response::build($status) ResponseBuilder::new($status)
} }
}; };
} }
@ -29,7 +29,6 @@ impl Response {
STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED); STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED);
STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES); STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES);
STATIC_RESP!(MovedPermanenty, StatusCode::MOVED_PERMANENTLY);
STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY); STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY);
STATIC_RESP!(Found, StatusCode::FOUND); STATIC_RESP!(Found, StatusCode::FOUND);
STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER); STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER);
@ -60,6 +59,8 @@ impl Response {
STATIC_RESP!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE); STATIC_RESP!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE);
STATIC_RESP!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE); STATIC_RESP!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE);
STATIC_RESP!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); STATIC_RESP!(ExpectationFailed, StatusCode::EXPECTATION_FAILED);
STATIC_RESP!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY);
STATIC_RESP!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS);
STATIC_RESP!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR); STATIC_RESP!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR);
STATIC_RESP!(NotImplemented, StatusCode::NOT_IMPLEMENTED); STATIC_RESP!(NotImplemented, StatusCode::NOT_IMPLEMENTED);

View File

@ -1,23 +1,16 @@
use std::cell::{Ref, RefMut}; use std::cell::{Ref, RefMut};
use std::str; use std::str;
use encoding::all::UTF_8; use encoding_rs::{Encoding, UTF_8};
use encoding::label::encoding_from_whatwg_label; use http::header;
use encoding::EncodingRef;
use http::{header, HeaderMap};
use mime::Mime; use mime::Mime;
use crate::error::{ContentTypeError, ParseError}; use crate::cookie::Cookie;
use crate::error::{ContentTypeError, CookieParseError, ParseError};
use crate::extensions::Extensions; use crate::extensions::Extensions;
use crate::header::Header; use crate::header::{Header, HeaderMap};
use crate::payload::Payload; use crate::payload::Payload;
#[cfg(feature = "cookies")]
use crate::error::CookieParseError;
#[cfg(feature = "cookies")]
use cookie::Cookie;
#[cfg(feature = "cookies")]
struct Cookies(Vec<Cookie<'static>>); struct Cookies(Vec<Cookie<'static>>);
/// Trait that implements general purpose operations on http messages /// Trait that implements general purpose operations on http messages
@ -32,10 +25,10 @@ pub trait HttpMessage: Sized {
fn take_payload(&mut self) -> Payload<Self::Stream>; fn take_payload(&mut self) -> Payload<Self::Stream>;
/// Request's extensions container /// Request's extensions container
fn extensions(&self) -> Ref<Extensions>; fn extensions(&self) -> Ref<'_, Extensions>;
/// 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>;
#[doc(hidden)] #[doc(hidden)]
/// Get a header /// Get a header
@ -64,10 +57,12 @@ pub trait HttpMessage: Sized {
/// Get content type encoding /// Get content type encoding
/// ///
/// UTF-8 is used by default, If request charset is not set. /// UTF-8 is used by default, If request charset is not set.
fn encoding(&self) -> Result<EncodingRef, ContentTypeError> { fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> {
if let Some(mime_type) = self.mime_type()? { if let Some(mime_type) = self.mime_type()? {
if let Some(charset) = mime_type.get_param("charset") { if let Some(charset) = mime_type.get_param("charset") {
if let Some(enc) = encoding_from_whatwg_label(charset.as_str()) { if let Some(enc) =
Encoding::for_label_no_replacement(charset.as_str().as_bytes())
{
Ok(enc) Ok(enc)
} else { } else {
Err(ContentTypeError::UnknownEncoding) Err(ContentTypeError::UnknownEncoding)
@ -110,8 +105,7 @@ pub trait HttpMessage: Sized {
/// Load request cookies. /// Load request cookies.
#[inline] #[inline]
#[cfg(feature = "cookies")] fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
fn cookies(&self) -> Result<Ref<Vec<Cookie<'static>>>, CookieParseError> {
if self.extensions().get::<Cookies>().is_none() { if self.extensions().get::<Cookies>().is_none() {
let mut cookies = Vec::new(); let mut cookies = Vec::new();
for hdr in self.headers().get_all(header::COOKIE) { for hdr in self.headers().get_all(header::COOKIE) {
@ -131,7 +125,6 @@ pub trait HttpMessage: Sized {
} }
/// Return request cookie. /// Return request cookie.
#[cfg(feature = "cookies")]
fn cookie(&self, name: &str) -> Option<Cookie<'static>> { fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
if let Ok(cookies) = self.cookies() { if let Ok(cookies) = self.cookies() {
for cookie in cookies.iter() { for cookie in cookies.iter() {
@ -160,12 +153,12 @@ where
} }
/// Request's extensions container /// Request's extensions container
fn extensions(&self) -> Ref<Extensions> { fn extensions(&self) -> Ref<'_, Extensions> {
(**self).extensions() (**self).extensions()
} }
/// 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> {
(**self).extensions_mut() (**self).extensions_mut()
} }
} }
@ -173,8 +166,7 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::Bytes; use bytes::Bytes;
use encoding::all::ISO_8859_2; use encoding_rs::ISO_8859_2;
use encoding::Encoding;
use mime; use mime;
use super::*; use super::*;
@ -230,7 +222,7 @@ mod tests {
"application/json; charset=ISO-8859-2", "application/json; charset=ISO-8859-2",
) )
.finish(); .finish();
assert_eq!(ISO_8859_2.name(), req.encoding().unwrap().name()); assert_eq!(ISO_8859_2, req.encoding().unwrap());
} }
#[test] #[test]

View File

@ -1,8 +1,10 @@
//! Basic http primitives for actix-net framework. //! Basic http primitives for actix-net framework.
#![deny(rust_2018_idioms, warnings)]
#![allow( #![allow(
clippy::type_complexity, clippy::type_complexity,
clippy::too_many_arguments,
clippy::new_without_default, clippy::new_without_default,
clippy::new_without_default_derive clippy::borrow_interior_mutable_const
)] )]
#[macro_use] #[macro_use]
@ -11,7 +13,9 @@ extern crate log;
pub mod body; pub mod body;
mod builder; mod builder;
pub mod client; pub mod client;
mod cloneable;
mod config; mod config;
#[cfg(feature = "compress")]
pub mod encoding; pub mod encoding;
mod extensions; mod extensions;
mod header; mod header;
@ -24,6 +28,7 @@ mod request;
mod response; mod response;
mod service; mod service;
pub mod cookie;
pub mod error; pub mod error;
pub mod h1; pub mod h1;
pub mod h2; pub mod h2;
@ -35,27 +40,23 @@ pub use self::config::{KeepAlive, ServiceConfig};
pub use self::error::{Error, ResponseError, Result}; pub use self::error::{Error, ResponseError, Result};
pub use self::extensions::Extensions; pub use self::extensions::Extensions;
pub use self::httpmessage::HttpMessage; pub use self::httpmessage::HttpMessage;
pub use self::message::{Message, RequestHead, 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, ResponseBuilder};
pub use self::service::{HttpService, SendError, SendResponse}; pub use self::service::HttpService;
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};
pub use http::uri::PathAndQuery;
pub use http::{uri, Error, Uri};
pub use http::{Method, StatusCode, Version}; pub use http::{Method, StatusCode, Version};
#[doc(hidden)] pub use crate::cookie::{Cookie, CookieBuilder};
pub use http::{uri, Error, HeaderMap, HttpTryFrom, Uri}; pub use crate::header::HeaderMap;
#[doc(hidden)]
pub use http::uri::PathAndQuery;
#[cfg(feature = "cookies")]
pub use cookie::{Cookie, CookieBuilder};
/// Various http headers /// Various http headers
pub mod header { pub mod header {
@ -64,3 +65,10 @@ pub mod http {
pub use crate::header::ContentEncoding; pub use crate::header::ContentEncoding;
pub use crate::message::ConnectionType; pub use crate::message::ConnectionType;
} }
/// Http protocol
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Protocol {
Http1,
Http2,
}

View File

@ -1,11 +1,13 @@
use std::cell::{Ref, RefCell, RefMut}; use std::cell::{Ref, RefCell, RefMut};
use std::collections::VecDeque; use std::net;
use std::rc::Rc; use std::rc::Rc;
use bitflags::bitflags; use bitflags::bitflags;
use copyless::BoxHelper;
use crate::extensions::Extensions; use crate::extensions::Extensions;
use crate::http::{header, HeaderMap, Method, StatusCode, Uri, Version}; use crate::header::HeaderMap;
use crate::http::{header, Method, StatusCode, Uri, Version};
/// Represents various types of connection /// Represents various types of connection
#[derive(Copy, Clone, PartialEq, Debug)] #[derive(Copy, Clone, PartialEq, Debug)]
@ -23,7 +25,9 @@ bitflags! {
const CLOSE = 0b0000_0001; const CLOSE = 0b0000_0001;
const KEEP_ALIVE = 0b0000_0010; const KEEP_ALIVE = 0b0000_0010;
const UPGRADE = 0b0000_0100; const UPGRADE = 0b0000_0100;
const NO_CHUNKING = 0b0000_1000; const EXPECT = 0b0000_1000;
const NO_CHUNKING = 0b0001_0000;
const CAMEL_CASE = 0b0010_0000;
} }
} }
@ -41,6 +45,7 @@ pub struct RequestHead {
pub version: Version, pub version: Version,
pub headers: HeaderMap, pub headers: HeaderMap,
pub extensions: RefCell<Extensions>, pub extensions: RefCell<Extensions>,
pub peer_addr: Option<net::SocketAddr>,
flags: Flags, flags: Flags,
} }
@ -52,6 +57,7 @@ impl Default for RequestHead {
version: Version::HTTP_11, version: Version::HTTP_11,
headers: HeaderMap::with_capacity(16), headers: HeaderMap::with_capacity(16),
flags: Flags::empty(), flags: Flags::empty(),
peer_addr: None,
extensions: RefCell::new(Extensions::new()), extensions: RefCell::new(Extensions::new()),
} }
} }
@ -72,13 +78,13 @@ impl Head for RequestHead {
impl RequestHead { impl RequestHead {
/// Message extensions /// Message extensions
#[inline] #[inline]
pub fn extensions(&self) -> Ref<Extensions> { pub fn extensions(&self) -> Ref<'_, Extensions> {
self.extensions.borrow() self.extensions.borrow()
} }
/// Mutable reference to a the message's extensions /// Mutable reference to a the message's extensions
#[inline] #[inline]
pub fn extensions_mut(&self) -> RefMut<Extensions> { pub fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.extensions.borrow_mut() self.extensions.borrow_mut()
} }
@ -92,6 +98,23 @@ impl RequestHead {
&mut self.headers &mut self.headers
} }
/// Is to uppercase headers with Camel-Case.
/// Befault is `false`
#[inline]
pub fn camel_case_headers(&self) -> bool {
self.flags.contains(Flags::CAMEL_CASE)
}
/// Set `true` to send headers which are uppercased with Camel-Case.
#[inline]
pub fn set_camel_case_headers(&mut self, val: bool) {
if val {
self.flags.insert(Flags::CAMEL_CASE);
} else {
self.flags.remove(Flags::CAMEL_CASE);
}
}
#[inline] #[inline]
/// Set connection type of the message /// Set connection type of the message
pub fn set_connection_type(&mut self, ctype: ConnectionType) { pub fn set_connection_type(&mut self, ctype: ConnectionType) {
@ -145,6 +168,47 @@ impl RequestHead {
self.flags.remove(Flags::NO_CHUNKING); self.flags.remove(Flags::NO_CHUNKING);
} }
} }
#[inline]
/// Request contains `EXPECT` header
pub fn expect(&self) -> bool {
self.flags.contains(Flags::EXPECT)
}
#[inline]
pub(crate) fn set_expect(&mut self) {
self.flags.insert(Flags::EXPECT);
}
}
#[derive(Debug)]
pub enum RequestHeadType {
Owned(RequestHead),
Rc(Rc<RequestHead>, Option<HeaderMap>),
}
impl RequestHeadType {
pub fn extra_headers(&self) -> Option<&HeaderMap> {
match self {
RequestHeadType::Owned(_) => None,
RequestHeadType::Rc(_, headers) => headers.as_ref(),
}
}
}
impl AsRef<RequestHead> for RequestHeadType {
fn as_ref(&self) -> &RequestHead {
match self {
RequestHeadType::Owned(head) => &head,
RequestHeadType::Rc(head, _) => head.as_ref(),
}
}
}
impl From<RequestHead> for RequestHeadType {
fn from(head: RequestHead) -> Self {
RequestHeadType::Owned(head)
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -157,41 +221,29 @@ pub struct ResponseHead {
flags: Flags, flags: Flags,
} }
impl Default for ResponseHead { impl ResponseHead {
fn default() -> ResponseHead { /// Create new instance of `ResponseHead` type
#[inline]
pub fn new(status: StatusCode) -> ResponseHead {
ResponseHead { ResponseHead {
status,
version: Version::default(), version: Version::default(),
status: StatusCode::OK, headers: HeaderMap::with_capacity(12),
headers: HeaderMap::with_capacity(16),
reason: None, reason: None,
flags: Flags::empty(), flags: Flags::empty(),
extensions: RefCell::new(Extensions::new()), extensions: RefCell::new(Extensions::new()),
} }
} }
}
impl Head for ResponseHead {
fn clear(&mut self) {
self.reason = None;
self.flags = Flags::empty();
self.headers.clear();
}
fn pool() -> &'static MessagePool<Self> {
RESPONSE_POOL.with(|p| *p)
}
}
impl ResponseHead {
/// Message extensions /// Message extensions
#[inline] #[inline]
pub fn extensions(&self) -> Ref<Extensions> { pub fn extensions(&self) -> Ref<'_, Extensions> {
self.extensions.borrow() self.extensions.borrow()
} }
/// Mutable reference to a the message's extensions /// Mutable reference to a the message's extensions
#[inline] #[inline]
pub fn extensions_mut(&self) -> RefMut<Extensions> { pub fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.extensions.borrow_mut() self.extensions.borrow_mut()
} }
@ -288,7 +340,6 @@ impl ResponseHead {
pub struct Message<T: Head> { pub struct Message<T: Head> {
head: Rc<T>, head: Rc<T>,
pool: &'static MessagePool<T>,
} }
impl<T: Head> Message<T> { impl<T: Head> Message<T> {
@ -302,7 +353,6 @@ impl<T: Head> Clone for Message<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Message { Message {
head: self.head.clone(), head: self.head.clone(),
pool: self.pool,
} }
} }
} }
@ -324,39 +374,79 @@ 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 { if Rc::strong_count(&self.head) == 1 {
self.pool.release(self.head.clone()); T::pool().release(self.head.clone());
}
}
}
pub(crate) struct BoxedResponseHead {
head: Option<Box<ResponseHead>>,
}
impl BoxedResponseHead {
/// Get new message from the pool of objects
pub fn new(status: StatusCode) -> Self {
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 {
type Target = ResponseHead;
fn deref(&self) -> &Self::Target {
self.head.as_ref().unwrap()
}
}
impl std::ops::DerefMut for BoxedResponseHead {
fn deref_mut(&mut self) -> &mut Self::Target {
self.head.as_mut().unwrap()
}
}
impl Drop for BoxedResponseHead {
fn drop(&mut self) {
if let Some(head) = self.head.take() {
RESPONSE_POOL.with(move |p| p.release(head))
} }
} }
} }
#[doc(hidden)] #[doc(hidden)]
/// Request's objects pool /// Request's objects pool
pub struct MessagePool<T: Head>(RefCell<VecDeque<Rc<T>>>); pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
#[doc(hidden)]
#[allow(clippy::vec_box)]
/// Request's objects pool
pub struct BoxedResponsePool(RefCell<Vec<Box<ResponseHead>>>);
thread_local!(static REQUEST_POOL: &'static MessagePool<RequestHead> = MessagePool::<RequestHead>::create()); thread_local!(static REQUEST_POOL: &'static MessagePool<RequestHead> = MessagePool::<RequestHead>::create());
thread_local!(static RESPONSE_POOL: &'static MessagePool<ResponseHead> = MessagePool::<ResponseHead>::create()); thread_local!(static RESPONSE_POOL: &'static BoxedResponsePool = BoxedResponsePool::create());
impl<T: Head> MessagePool<T> { impl<T: Head> MessagePool<T> {
fn create() -> &'static MessagePool<T> { fn create() -> &'static MessagePool<T> {
let pool = MessagePool(RefCell::new(VecDeque::with_capacity(128))); let pool = MessagePool(RefCell::new(Vec::with_capacity(128)));
Box::leak(Box::new(pool)) 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(&'static self) -> Message<T> {
if let Some(mut msg) = self.0.borrow_mut().pop_front() { if let Some(mut msg) = self.0.borrow_mut().pop() {
if let Some(r) = Rc::get_mut(&mut msg) { if let Some(r) = Rc::get_mut(&mut msg) {
r.clear(); r.clear();
} }
Message { Message { head: msg }
head: msg,
pool: self,
}
} else { } else {
Message { Message {
head: Rc::new(T::default()), head: Rc::new(T::default()),
pool: self,
} }
} }
} }
@ -366,7 +456,40 @@ impl<T: Head> MessagePool<T> {
fn release(&self, msg: Rc<T>) { fn release(&self, msg: Rc<T>) {
let v = &mut self.0.borrow_mut(); let v = &mut self.0.borrow_mut();
if v.len() < 128 { if v.len() < 128 {
v.push_front(msg); v.push(msg);
}
}
}
impl BoxedResponsePool {
fn create() -> &'static BoxedResponsePool {
let pool = BoxedResponsePool(RefCell::new(Vec::with_capacity(128)));
Box::leak(Box::new(pool))
}
/// Get message from the pool
#[inline]
fn get_message(&'static self, status: StatusCode) -> BoxedResponseHead {
if let Some(mut head) = self.0.borrow_mut().pop() {
head.reason = None;
head.status = status;
head.headers.clear();
head.flags = Flags::empty();
BoxedResponseHead { head: Some(head) }
} else {
BoxedResponseHead {
head: Some(Box::alloc().init(ResponseHead::new(status))),
}
}
}
#[inline]
/// Release request instance
fn release(&self, msg: Box<ResponseHead>) {
let v = &mut self.0.borrow_mut();
if v.len() < 128 {
msg.extensions.borrow_mut().clear();
v.push(msg);
} }
} }
} }

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