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

Compare commits

...

1320 Commits

Author SHA1 Message Date
1fcf1d4a49 prepare release 2018-06-21 09:47:46 +06:00
4012606910 SendRequest execution fails with the entered unreachable code #329 2018-06-21 09:47:28 +06:00
e975124630 Allow to disable masking for websockets client 2018-06-21 09:38:59 +06:00
6862aa6ee7 prepare release 2018-06-13 05:04:59 -07:00
8a22558f25 http/2 end-of-frame is not set if body is empty bytes #307 2018-06-12 14:47:45 -07:00
b5b9f9656e do not allow stream or actor responses for internal error #301 2018-06-11 19:44:11 -07:00
2fffc55d34 update changelog 2018-06-11 18:53:36 -07:00
7d39f1582e InternalError can trigger memory unsafety #301 2018-06-11 18:52:54 -07:00
75ed053a35 bump version 2018-06-11 12:32:31 -07:00
cfedf5fff4 Merge branch '0.6' of github.com:actix/actix-web into 0.6 2018-06-11 12:32:05 -07:00
be73a36339 use custom resolver 2018-06-11 12:28:43 -07:00
1ad8ba2604 Fix docs.rs build 2018-06-11 12:25:20 -07:00
6848a12095 prepare 0.6.12 release 2018-06-09 01:22:19 +02:00
4797298706 Allow to use custom resolver for ClientConnector 2018-06-08 16:10:47 -07:00
5eaf4cbefd update changelog 2018-06-08 08:42:10 -07:00
7f1844e541 fix doc test 2018-06-07 20:22:50 -07:00
6c7ac7fc22 update changelog 2018-06-07 20:07:58 -07:00
42f9e1034b add Host predicate 2018-06-07 20:05:45 -07:00
e3cd0fdd13 add application filters 2018-06-07 20:05:26 -07:00
40ff550460 update changelog 2018-06-07 20:04:40 -07:00
7119340d44 Added improved failure interoperability with downcasting (#285)
Deprecates Error::cause and introduces failure interoperability functions and downcasting.
2018-06-07 20:03:10 -07:00
e140bc3906 prep release 2018-06-05 09:44:29 -07:00
fdc08d365d metadata for docs.rs 2018-06-05 08:59:30 -07:00
8f1b88e39e update changelog 2018-06-05 08:53:27 -07:00
6a40a0a466 fix multipart boundary parsing #282 2018-06-05 08:52:46 -07:00
89fc6b6ac9 changelog 2018-06-05 07:42:52 -07:00
afa67b838a CORS: Do not validate Origin header on non-OPTION requests #271 2018-06-05 07:41:13 -07:00
f7b7d282bf Middleware::response is not invoked if error result was returned by another Middleware::start #255 2018-06-04 13:57:54 -07:00
09780ea9f3 changelog updates 2018-06-02 15:03:34 -07:00
a7dab950f3 Support chunked encoding for UrlEncoded body #262 2018-06-02 15:02:42 -07:00
ec0737e392 fix doc test 2018-06-02 13:48:37 -07:00
d664993d56 remove debug prints 2018-06-02 11:58:11 -07:00
a9c6c57a67 remove debug print 2018-06-02 11:55:27 -07:00
08e7374eee update changelog 2018-06-02 11:46:53 -07:00
42da1448fb fixed HttpRequest::url_for for a named route with no variables #265 2018-06-02 11:46:02 -07:00
9f9e0b98ad change homepage link 2018-05-24 08:55:10 -07:00
556646aaec update changelog 2018-05-24 07:56:51 -07:00
174fb0b5f4 Merge pull request #239 from max-frai/master
Add ability to set encoding for exact NamedFile.
2018-05-24 07:46:53 -07:00
836706653b Merge branch 'master' into master 2018-05-24 07:46:46 -07:00
17f1a2b92a more scope tests 2018-05-23 14:11:01 -07:00
3b08b16c11 bump version 2018-05-23 13:21:54 -07:00
68eb2f26c9 Allow to use path without traling slashes for scope registration #241 2018-05-23 13:21:29 -07:00
72757887c9 update doc links 2018-05-23 11:20:12 -07:00
eb5dbd43ae update changelog 2018-05-23 10:37:17 -07:00
1f1dfac3f9 Merge pull request #240 from ivanovaleksey/patch-2
Fix TestServer::post
2018-05-23 09:50:40 -07:00
2479b14aba Fix TestServer::post 2018-05-23 19:07:42 +03:00
ac24703512 Add ability to set encoding for exact NamedFile. 2018-05-23 09:12:23 +03:00
db0091ba6f disable server test for windows 2018-05-21 21:01:52 -07:00
2159158c30 Fix streaming response with body compression 2018-05-21 20:50:10 -07:00
76d790425f bump version 2018-05-21 19:07:56 -07:00
90968d4333 Drop connection if request's payload is not fulle consumed #236 2018-05-21 18:54:17 -07:00
577a509875 increase delay 2018-05-21 16:12:33 -07:00
a9728abfc8 run coverage report on 1.24 2018-05-20 21:10:50 -07:00
14d1b8e2b6 prepare release 2018-05-20 21:09:54 -07:00
285c73e95e Re-use tcp listener on pause/resume 2018-05-20 20:47:20 -07:00
483db7028c expose low level data 2018-05-20 20:37:19 -07:00
082ff46041 Fix scope resource path extractor #234 2018-05-20 17:04:23 -07:00
f32e8f22c8 Merge pull request #231 from qrvaelet/ranges
NamedFile: range upgrade
2018-05-20 09:18:46 -07:00
766dde7c42 Merge branch 'master' into ranges 2018-05-20 08:51:07 -07:00
b68687044e range header syntax fix, change range to content-range in responses, enabled accept ranges, tests for content-range, content-length, and range status code 2018-05-20 17:40:36 +02:00
c9e84e9dd3 Merge pull request #233 from sindreij/patch-1
Fix some typos in server/srv.rs
2018-05-20 06:19:53 -07:00
0126ac46fc Fix some typos in server/srv.rs
Hello! This looks like a great library, thanks for creating it! While reading through the documentation I found a few typos.
2018-05-20 14:43:26 +02:00
9b7ea836d0 bump version 2018-05-17 18:34:09 -07:00
537b420d35 Fix compilation with --no-default-features 2018-05-17 18:33:48 -07:00
16906c5951 clippy warnings 2018-05-17 12:23:37 -07:00
45e9aaa462 rustfmt 0.7 2018-05-17 12:20:20 -07:00
564cc15c04 update changes 2018-05-17 12:20:04 -07:00
8fd18d56a5 Merge pull request #227 from qrvaelet/ranges
NamedFile: added basic ranges header support, added content-length support
2018-05-17 12:18:10 -07:00
a5692d4ecf Merge branch 'master' into ranges 2018-05-17 11:16:08 -07:00
2d83f79433 NamedFile: added ranges support, content-length support 2018-05-17 20:09:41 +02:00
f3ece74406 better error handling 2018-05-17 10:58:08 -07:00
8de1f60347 add session extractor doc api 2018-05-16 21:05:59 -07:00
b4252f8fd1 implement extractor for Session 2018-05-16 21:02:51 -07:00
fe2b50a9ef update changelog 2018-05-16 11:02:50 -07:00
d8ae8c3821 Merge pull request #225 from mitsuhiko/feature/addrs-with-scheme
Support returning addresses plus scheme from the server
2018-05-16 11:02:15 -07:00
c9a026fabb Merge branch 'master' into feature/addrs-with-scheme 2018-05-16 11:01:45 -07:00
64eca1546e Merge pull request #224 from mitsuhiko/feature/listen-tls
Add support for listen_tls/listen_ssl
2018-05-16 11:01:28 -07:00
b19fe98ff4 Merge branch 'master' into feature/listen-tls 2018-05-16 11:01:21 -07:00
b393ddf879 fix panic during middleware execution #226 2018-05-16 11:00:29 -07:00
7bb7d85c1d Added support for returning addresses plus scheme from the server 2018-05-16 16:17:27 +02:00
6e976153e7 Add support for listen_tls/listen_ssl 2018-05-16 15:20:47 +02:00
03e758cee4 bump version 2018-05-15 19:08:34 -07:00
0d36b8f826 fix 1.24 compatibility 2018-05-15 19:07:43 -07:00
f82fa08d72 various optimizations 2018-05-15 16:49:03 -07:00
d6787e6c56 prepare release 2018-05-15 10:20:32 -07:00
b9d870645f store cookies in extensions 2018-05-15 10:09:48 -07:00
ef89430f9b undeprecate query() and store query in extensions 2018-05-15 09:53:58 -07:00
953a0d4e4a add test case for #222 2018-05-15 09:29:59 -07:00
5ea2d68438 h1 decoder blocks on error #222 2018-05-15 07:55:36 -07:00
d65a03f6ac use latest nightly for appveyor 2018-05-13 08:43:09 -07:00
b588b2bf5c Merge pull request #221 from skorgu/patch-1
Include mention of http client in README.md
2018-05-11 21:56:46 -07:00
d455e2cd13 Merge branch 'master' into patch-1 2018-05-11 21:56:35 -07:00
9306631d6e Fix segfault in ServerSettings::get_response_builder() 2018-05-11 21:19:48 -07:00
f735da504b Include mention of http client in README.md 2018-05-11 20:36:54 -04:00
487a713ca0 update doc string 2018-05-11 15:01:15 -07:00
095ad328ee prepare release 2018-05-10 15:45:06 -07:00
a38afa0cec --no-count for tarpaulin 2018-05-10 13:05:56 -07:00
9619698543 doc string 2018-05-10 13:04:56 -07:00
4b1a471b35 add more examples for extractor config 2018-05-10 13:03:43 -07:00
b6039b0bff add doc string 2018-05-10 11:04:03 -07:00
d8fa43034f export ExtractorConfig type 2018-05-10 11:00:22 -07:00
92f993e054 Fix client request timeout handling 2018-05-10 09:37:38 -07:00
c172deb0f3 Merge pull request #219 from benjamingroeber/improve-readme
correct order of format arguments in readme example
2018-05-10 09:15:50 -07:00
dee6aed010 Merge branch 'master' into improve-readme 2018-05-10 09:15:44 -07:00
76f021a6e3 add tests for ErrorXXX helpers 2018-05-10 09:13:26 -07:00
2f244ea028 fix order of name and id in readme example 2018-05-10 18:12:59 +02:00
5f5ddc8f01 Merge pull request #218 from Dowwie/master
added error response functions for 501,502,503,504
2018-05-10 08:59:23 -07:00
8b473745cb added error response functions for 501,502,503,504 2018-05-10 11:26:38 -04:00
18575ee1ee Add Router::with_async() method for async handler registration 2018-05-09 16:27:31 -07:00
e58b38fd13 deprecate WsWrite from top level mod 2018-05-09 06:12:16 -07:00
b043c34632 bump version 2018-05-09 06:05:44 -07:00
b748bf3b0d make api public 2018-05-09 06:05:16 -07:00
be12d5e6fc make WsWriter trait optional 2018-05-09 05:48:06 -07:00
7c4941f868 update migration doc 2018-05-08 18:48:09 -07:00
d1f5c457c4 Merge branch 'master' of github.com:actix/actix-web 2018-05-08 18:35:52 -07:00
c26c5fd9a4 prep release 2018-05-08 18:34:36 -07:00
4a73d1c8c1 Merge pull request #216 from lcowell/lcowell-scoupe
replace typo `scoupe` with `scope`
2018-05-08 17:47:20 -07:00
7c395fcc83 replace typo scoupe with scope 2018-05-08 17:40:18 -07:00
54c33a7aff Allow to exclude certain endpoints from logging #211 2018-05-08 16:30:34 -07:00
47d80382b2 Fix http/2 payload streaming #215 2018-05-08 15:44:50 -07:00
ba816a8562 Merge pull request #214 from niklasf/de-path-404
let Path::from_request() fail with ErrorNotFound
2018-05-08 14:41:05 -07:00
6f75b0e95e let Path::from_request() fail with ErrorNotFound 2018-05-08 22:59:46 +02:00
b3cc43bb9b Fix connector's default keep-alive and lifetime settings #212 2018-05-08 13:41:04 -07:00
ecda97aadd update doc string 2018-05-08 05:54:06 -07:00
8cda362866 simplify pipeline 2018-05-07 16:09:41 -07:00
3c6c1268c9 travis cover report 2018-05-07 15:54:29 -07:00
72908d974c test for Scope::route(); prep release 2018-05-07 15:19:03 -07:00
c755d71a8b add filters support to scopes 2018-05-07 14:40:04 -07:00
a817ddb57b add variable segments support for scope prefix 2018-05-07 13:50:43 -07:00
44c36e93d1 Merge pull request #210 from andreevlex/feature/spelling-check-06-05
spelling check
2018-05-07 11:30:46 -07:00
c92ebc22d7 Merge branch 'master' into feature/spelling-check-06-05 2018-05-07 11:30:39 -07:00
599fd6af93 fix formatting 2018-05-07 20:53:45 +03:00
fa81d97004 more handler tests 2018-05-06 20:05:31 -07:00
c54f045b39 more handler tests 2018-05-06 15:11:36 -07:00
cd11293c1f spelling check 2018-05-06 19:07:30 +03:00
45325a5f75 more middleware tests 2018-05-06 08:33:41 -07:00
a7c40024ce async handle middleware test 2018-05-05 18:40:16 -07:00
0af4d01fe4 move middleware tests to seprate module 2018-05-05 12:18:43 -07:00
bd6e18b7fe update migration doc 2018-05-04 13:38:17 -07:00
f66cf16823 upgrade regex 2018-05-04 12:25:06 -07:00
03d6b04eef update tests 2018-05-04 12:11:38 -07:00
f37880d89c refactor Responder trait 2018-05-04 11:44:22 -07:00
8b43574bd5 Merge branch 'master' of github.com:actix/actix-web 2018-05-03 16:27:12 -07:00
b07d0e712f always provide backtrace for error 2018-05-03 16:26:42 -07:00
acd7380865 rename Reply to a AsyncResult 2018-05-03 16:22:08 -07:00
0208dfb6b2 Merge pull request #208 from DenisKolodin/ws-trait
Add WsWriter trait
2018-05-03 10:43:44 -07:00
bb61dd41af Merge branch 'master' into ws-trait 2018-05-03 08:57:45 -07:00
58079b5bbe add session test 2018-05-02 19:11:44 -07:00
3623383e83 fix tests 2018-05-02 16:48:42 -07:00
7036656ae4 make Reply generic over error too 2018-05-02 16:33:29 -07:00
32a2866449 Allow to override files listing renderer for #203 2018-05-02 15:53:07 -07:00
35a4078434 update changelog 2018-05-02 13:43:51 -07:00
4ca5d8bcfc add FromRequest impl for tuples of various length 2018-05-02 13:38:25 -07:00
a38acb41e5 better query example 2018-05-02 06:30:06 -07:00
31e23d4ab1 add query deprecation info 2018-05-02 06:28:38 -07:00
1aadfee6f7 rename from_default to extract 2018-05-02 06:09:50 -07:00
76b644365f use read only ref for FromRequest; remove unnecessary static 2018-05-02 06:07:30 -07:00
80f385e703 Add WsWriter trait
`WsWriter` trait is a common interface for writing to a websocket and
it's implemented for both: `WebScoketContext` and `ClientWriter`.
2018-05-02 08:35:50 +03:00
a1958deaae add impl Future for Reply 2018-05-01 17:30:06 -07:00
8d65468c58 refactor FromRequest trait 2018-05-01 17:19:15 -07:00
195246573e rename threads to workers 2018-05-01 13:15:35 -07:00
e01102bda2 no need for mut 2018-05-01 11:45:46 -07:00
9b6343d54b refactor session impl 2018-05-01 09:40:23 -07:00
d9a4fadaae make HttpRequest::extensions() readonly 2018-05-01 09:05:50 -07:00
48e05a2d87 add nested scope support 2018-04-30 22:04:24 -07:00
70d0c5c700 update changes 2018-04-30 19:56:17 -07:00
d43ca96c5c Allow to use ssl and non-ssl connections with the same HttpServer #206 2018-04-30 19:51:55 -07:00
bfd46e6a71 update doc string 2018-04-29 22:28:16 -07:00
25b245ac72 allow to use custom state for scope 2018-04-29 22:19:52 -07:00
eefbe19651 remove deprecated types and methods 2018-04-29 21:05:10 -07:00
ab4e889f96 add middleware finished handler for route middleware 2018-04-29 20:50:38 -07:00
91235ac816 fix reading from socket 2018-04-29 20:34:59 -07:00
9c1bda3eca fix stable compiler compatibility 2018-04-29 19:49:26 -07:00
4a29f12876 update doc string; missing file 2018-04-29 19:39:28 -07:00
368730f5f1 Add route scopes #202 2018-04-29 19:35:50 -07:00
aa757a5be8 Allow to access Error's backtrace object 2018-04-29 14:21:50 -07:00
03ded62337 bump minimum supported rustc version because of minor version change of parking_lot crate 2018-04-29 14:13:46 -07:00
c72d1381a6 clippy warnings 2018-04-29 09:09:08 -07:00
d98d723f97 bump rustc version requirements 2018-04-29 08:24:19 -07:00
eb6e618812 Merge pull request #204 from svenstaro/master
Add Content-Disposition to NamedFile (fixes #172)
2018-04-29 08:18:48 -07:00
de222fe33b Merge and fix PR comments 2018-04-29 14:02:50 +02:00
de49796fd1 clippy warnings; fmt 2018-04-28 22:55:47 -07:00
a38c3985f6 refactor http1 parser 2018-04-28 22:20:32 -07:00
492c072564 Add Content-Disposition to NamedFile (fixes #172) 2018-04-27 09:49:55 +02:00
fd876efa68 allow to access application state during configuration stage 2018-04-26 09:05:07 -07:00
c5b9bed478 update changes 2018-04-26 08:01:08 -07:00
3eba383cdc Merge pull request #196 from fuchsnj/websocket_close_reason
Websocket close reason
2018-04-26 07:55:32 -07:00
927f2e594e Merge branch 'master' into websocket_close_reason 2018-04-25 20:17:19 -07:00
fa9edf2180 prep release 2018-04-24 12:34:10 -07:00
5ca904d1db make flate crate optional 2018-04-24 12:24:04 -07:00
2e7d323e1a add r2d2 example link 2018-04-24 09:34:38 -07:00
b66566f610 comments 2018-04-24 09:32:19 -07:00
2477afcf30 Allow to use rust backend for flate2 crate #199 2018-04-24 09:29:15 -07:00
bcd03a9c62 link to askama example 2018-04-24 09:16:46 -07:00
f8af3ef7f4 refactor keep-alive 2018-04-22 15:28:04 -07:00
f8b75c157f fix style 2018-04-22 11:43:47 -04:00
b7b61afacc add ws close description parse test 2018-04-21 17:20:23 -04:00
507361c1df Merge branch 'master' into websocket_close_reason 2018-04-21 17:05:43 -04:00
f6fd9e70f9 code cleanup 2018-04-21 16:53:55 -04:00
de8a09254d use Optional with websocket close reason 2018-04-21 16:50:27 -04:00
f89b7a9bb8 Merge pull request #194 from actix/brandur-allowed-origin-into
Let CSRF's `allowed_origin()` be specified as a type supporting `Into<String>`
2018-04-21 10:37:18 -07:00
59244b203c Let CSRF's allowed_origin() be specified as a type supporting Into<String>
A very minor addition: I'm using this middleware on specific resources,
and given a non-static string, I often have to `clone()` already to get
a string into a closure. Take this code for example:

``` rust
let server = actix_web::server::new(move || {
    let csrf_origin_graphql = csrf_origin.clone();

    ...

    .resource("/graphql", move |r| {
	r.middleware(
	    csrf::CsrfFilter::new().allowed_origin(csrf_origin_graphql.as_str()),
	);

	r.method(Method::POST).a(graphql::handlers::graphql_post);
    })
```

Letting `allowed_origin()` take an `Into<String>` instead of `&str` would
prevent a second `clone()` in the code above, and also make the code a little
nicer to read (you eliminate the `.as_str()` above). This is a pattern that
seems to be common throughout actix-web already anyway, so it should also be
fine to have here.
2018-04-21 08:41:06 -07:00
2adf8a3a48 add changelog entry 2018-04-21 07:56:11 -07:00
805dbea8e7 Merge pull request #192 from fuchsnj/check_if_close_code_exists
check if close code exists before reading it
2018-04-21 07:54:25 -07:00
dc9a24a189 add websocket empty close status test 2018-04-20 21:55:07 -04:00
5528cf62f0 check if close code exists before reading it 2018-04-20 21:30:18 -04:00
9880a95603 Merge pull request #189 from drklee3/patch-1
Update README links to use new guide
2018-04-19 19:24:40 -07:00
2579c49865 Update README links to use new guide 2018-04-19 18:51:01 -07:00
01a0f3f5a0 remove unused dependency 2018-04-19 09:54:22 -07:00
2c8d987241 Use Display formatting for InternalError Display implementation #188 2018-04-19 07:55:09 -07:00
813d1d6e66 doc strings layout 2018-04-18 20:41:03 -07:00
48b02abee7 fmt 2018-04-18 20:16:29 -07:00
ce1081432b export session module 2018-04-18 20:11:49 -07:00
e9bdba57a0 Add identity service middleware 2018-04-18 19:05:24 -07:00
f907be585e Middleware response() is not invoked if there was an error in async handler #187 2018-04-18 14:15:53 -07:00
022f9800ed formatting 2018-04-18 10:49:03 -07:00
a9a54ac4c6 prep release 2018-04-18 10:45:59 -07:00
50b9fee3a7 Merge branch 'master' of github.com:actix/actix-web 2018-04-17 16:24:02 -07:00
bf9a90293f fix doc strings 2018-04-17 16:22:25 -07:00
17ec3a3a26 Merge pull request #185 from kornelski/master
Replace use of try!() with ?
2018-04-17 15:57:09 -07:00
5b4b885fd6 Replace use of try!() with ? 2018-04-17 23:20:47 +01:00
65b8197876 better doc string for Application::with_state() 2018-04-17 13:59:55 -07:00
a826d113ee add custom request path quoter #182 2018-04-17 12:55:13 -07:00
3a79505a44 update doc string 2018-04-17 07:51:06 -07:00
5f3a7a6a52 Merge pull request #184 from ivanovaleksey/patch-1
Fix route in App::resource example
2018-04-17 07:49:09 -07:00
6a7b097bcf Fix route in App::resource example 2018-04-17 16:01:34 +03:00
30a36bed9d fix doc example 2018-04-16 09:50:37 -07:00
79818560b2 cleanup doc strings; prepare release 2018-04-16 09:30:59 -07:00
58cc0dfbc5 Fix Client Request with custom Body Stream halting on certain size requests #176 2018-04-15 10:22:09 -07:00
a9ea649348 Allow to configure StaticFiles CpuPool, via static method or env variable 2018-04-13 19:46:14 -07:00
634c5723a0 update changelog 2018-04-13 19:19:30 -07:00
a5b5ff0894 update doc strings 2018-04-13 19:14:14 -07:00
5140fea8d1 allow to use castom error handler for json extractor 2018-04-13 19:10:42 -07:00
333b4f57d3 use different directory for tests 2018-04-13 17:00:18 -07:00
827ca5eada remove skeptic tests 2018-04-13 16:36:39 -07:00
ebc1f6eff9 drop skeptic 2018-04-13 16:21:57 -07:00
a8567da3e2 move guide to separate repo; update links 2018-04-13 16:20:23 -07:00
113f5ad1a8 add rustfmt config 2018-04-13 16:02:01 -07:00
95f6277007 fix typo 2018-04-13 14:36:07 -07:00
22c776f46e Fix StaticFiles does not support percent encoded paths #177 2018-04-13 10:13:12 -07:00
c0976bfa17 fix test 2018-04-12 21:28:17 -07:00
5e9ec4299c fix workspace links 2018-04-12 20:52:30 -07:00
e05aba65de examples moved to separate repo 2018-04-12 20:31:58 -07:00
c5b18c6d30 prepare release 2018-04-12 16:03:22 -07:00
94c5bb5cdd add helper method for returning inner value 2018-04-12 15:55:15 -07:00
2ca0ea70c4 use one default cpu pool for StaticFiles #174 2018-04-12 15:50:20 -07:00
0b01884fca add timeouts stats to client connector 2018-04-12 13:08:13 -07:00
83168731fc update user guide content compression section 2018-04-12 09:54:35 -07:00
7295846426 Merge pull request #173 from jannic/pr
fix end-of-stream handling in parse_payload
2018-04-12 09:30:26 -07:00
72bc1546c4 fix end-of-stream handling in parse_payload
parse_payload can be called with a pre-filled buf.

In this case, it's totaly fine for read_from_io to return
sync::Ready(0) while buf is not empty. This is not an
PayloadError::Incomplete.

So, move the check for PayloadError::Incomplete down to the
decoding code: If the decoder is not ready, but the input stream
is finished, PayloadError::Incomplete will be returned.
2018-04-12 09:47:32 +02:00
d39b531537 Merge branch 'master' of github.com:actix/actix-web 2018-04-11 19:05:34 -07:00
35e68723df use older mdbook 2018-04-11 19:05:14 -07:00
0624f9b9d9 Update MIGRATION-0.4-0.5.md 2018-04-11 16:53:27 -07:00
0e3820afdf Update MIGRATION-0.4-0.5.md 2018-04-11 16:49:45 -07:00
839d67ac6a migration to 0.5 2018-04-11 16:46:21 -07:00
b517957761 fix stats for tls and alpn features 2018-04-11 16:34:01 -07:00
d18f9c5905 add clinet connector stats 2018-04-11 16:11:11 -07:00
76fcdc13a3 Merge pull request #171 from DoumanAsh/without_state_public
Make HttpRequest::without_state public
2018-04-11 23:48:19 +03:00
62a9b4c53c Rename HttpRequest::without_state into drop_state and make it public 2018-04-11 22:41:06 +03:00
c570229351 Update README.md 2018-04-11 10:49:34 -07:00
d041df6c4b update links 2018-04-10 19:27:09 -07:00
bc28e54976 add homepage link 2018-04-10 19:20:21 -07:00
26ab5cbd01 forgot to include 2018-04-10 15:14:46 -07:00
50c2a5ceb0 update basic example 2018-04-10 14:45:03 -07:00
8dbbb0ee07 update guide 2018-04-10 13:31:10 -07:00
ca76dff5a7 update redis example 2018-04-10 13:21:54 -07:00
88f66d49d0 openssl features 2018-04-10 11:07:54 -07:00
be288fa00a for NamedFile process etag and last modified only if status code is 200 2018-04-10 10:57:53 -07:00
5e6a0aa3df simplier example in readme 2018-04-10 10:39:16 -07:00
fd87eb59f8 remove reference to master 2018-04-10 10:29:10 -07:00
81ac905c7b fix prefix and static file serving #168 2018-04-10 10:16:00 -07:00
bb11fb3d24 update client mod doc string 2018-04-09 21:57:40 -07:00
23eea54776 update cors doc string 2018-04-09 21:39:32 -07:00
2881859400 proper test for CorsBuilder::resource 2018-04-09 21:29:57 -07:00
1686682c19 extend CorsBuilder api to make it more user friendly 2018-04-09 21:11:15 -07:00
d04ff13955 update version 2018-04-09 14:27:13 -07:00
e757dc5a71 clippy warnings 2018-04-09 14:25:30 -07:00
be358db422 CorsBuilder::finish() panics on any configuration error 2018-04-09 14:20:12 -07:00
7df2d6b12a clippy warnings; extend url_for example in user guide 2018-04-09 13:30:38 -07:00
458e6bdcc2 Merge pull request #170 from adwhit/private-cookies
Public, signed and private cookies
2018-04-09 12:54:15 -07:00
0b0bbd6bd9 Merge branch 'master' into private-cookies 2018-04-09 12:54:08 -07:00
5617896780 cleanup doc tests 2018-04-09 10:40:12 -07:00
2b803f30c9 remove CookieSessionBackend::new 2018-04-09 18:33:29 +01:00
9b152acc32 add signed and private cookies 2018-04-09 17:59:28 +01:00
eb66685d1a simplify csrf middleware 2018-04-09 09:49:07 -07:00
b505e682d4 fix session doc test 2018-04-09 09:31:11 -07:00
48e7013997 update guide examples 2018-04-09 07:57:50 -07:00
ff14633b3d simplify CookieSessionBackend; expose max_age cookie setting 2018-04-08 11:05:37 -07:00
37db7d8168 allow to override status code for NamedFile 2018-04-08 10:53:58 -07:00
89bf12605d Merge pull request #165 from tazjin/docs/various-doc-fixes
Various minor documentation fixes
2018-04-07 09:53:19 -07:00
9fb0498437 docs(lib): Add a note about getting started with the API docs
Adds some initial pointers for newcomers to the documentation that
direct them at some of the most commonly used API types.

I based these links on what *I* usually end up looking at when I open
the actix_web docs.
2018-04-07 17:27:53 +02:00
b2a43a3c8d docs(application): Formatting & spelling fixes in module docs 2018-04-07 17:19:11 +02:00
38063b9873 docs(client): Minor formatting and spelling fixes in module docs 2018-04-07 17:00:57 +02:00
1045a6c6f0 docs(README): Minor formatting and spelling fixes 2018-04-07 17:00:39 +02:00
7243c58fce stable rust compatibility 2018-04-06 21:57:45 -07:00
fffaf2bb2d App::route method 2018-04-06 21:18:42 -07:00
7becb95a97 fix guide example 2018-04-06 20:24:49 -07:00
a4b837a1c1 flaky test 2018-04-06 19:45:14 -07:00
542315ce7f simplify StaticFiles 2018-04-06 19:34:55 -07:00
602d78b76c Merge pull request #163 from memoryruins/guide
Guide: edits to the second half
2018-04-06 19:11:12 -07:00
18b706d4fb Guide: tweak to websocket and testing. 2018-04-06 19:44:52 -04:00
94b41fd484 Guide: tweak to database integration. 2018-04-06 19:42:18 -04:00
3a80cb7bf3 Guide: tweak to http/2. 2018-04-06 19:37:14 -04:00
e4a85a53f4 Guide: additional tweaks to Middleware. 2018-04-06 19:35:11 -04:00
1a45dbd768 Guide: additional tweak to testing chapter. 2018-04-06 19:26:07 -04:00
7cff5d9ade Guide: additional tweaks to request and response chapter. 2018-04-06 19:17:03 -04:00
c04e0fdec4 Merge branch 'master' into guide 2018-04-06 19:04:50 -04:00
0f0fe5f148 Guide: updates to the Database integration chapter. 2018-04-06 19:02:11 -04:00
e7f9f5b46d Guide: updates to HTTP/2 chapter. 2018-04-06 18:46:56 -04:00
c3fbba2678 Guide: updates to static file handling chapter. 2018-04-06 18:40:57 -04:00
a88e97edba Guide: updates to Middleware chapter. 2018-04-06 18:29:18 -04:00
1f08100f6f Guide: updates to the WebSockets chapter. 2018-04-06 18:04:42 -04:00
ab60ec6e1d Guide: updates to the Testing chapter. 2018-04-06 18:03:30 -04:00
0fbd05009d Guide: tweaks to the request and response chapter. 2018-04-06 17:31:18 -04:00
fdb7419e24 use actix-web from master 2018-04-06 14:11:04 -07:00
191b53bd7c pin futures 0.1 2018-04-06 13:22:27 -07:00
2d4ee0ee01 make Pause::new public 2018-04-06 12:34:24 -07:00
5bd5f67d79 add Pause message constructors 2018-04-06 12:31:31 -07:00
5d8cbccfe9 Remove article. 2018-04-06 15:12:06 -04:00
8d5fa6ee71 added Pause/Resume for client connector 2018-04-06 11:08:41 -07:00
084104d058 update doc strings for extractors 2018-04-06 10:24:57 -07:00
2c411a04a9 no need for export in doc example 2018-04-06 10:15:06 -07:00
af0c8d893d add shortcut method for client requests 2018-04-06 10:09:31 -07:00
691457fbfe update tests 2018-04-06 09:45:10 -07:00
2dafd9c681 do not re-export HttpServer from server module 2018-04-06 08:40:11 -07:00
12586db15c Merge pull request #160 from memoryruins/guide
Guide: edits to first half
2018-04-05 19:44:42 -07:00
b847bda8ca Merge branch 'master' into guide 2018-04-05 19:44:34 -07:00
2a543001e0 Tweaks to the URL Dispatch chapter. 2018-04-05 22:12:20 -04:00
0f86c596fa Tweaks to Errors chapter. 2018-04-05 21:54:39 -04:00
6c55501252 client connector wait timeout 2018-04-05 18:33:58 -07:00
961edfd21a Tweaks to the Handler chapter. 2018-04-05 21:30:52 -04:00
7f0de705a3 Tweaks to Server chapter. 2018-04-05 20:55:19 -04:00
c2ad65a61d Various tweaks to Application chapter. 2018-04-05 19:43:17 -04:00
3c93e0c654 Add newline for reading source. 2018-04-05 19:25:41 -04:00
a0f1ff7eb3 Add src directory to main.rs and list on first codeblock. 2018-04-05 19:21:29 -04:00
9f45cfe492 Expand note about actix. 2018-04-05 19:12:23 -04:00
46e6641528 Add repository hyperlink and trim repeat. 2018-04-05 18:46:36 -04:00
a3f124685a Remove redundant quickstart paragraph. 2018-04-05 18:32:04 -04:00
800f711cc1 add PayloadConfig 2018-04-04 21:13:48 -07:00
7be4b1f399 clippy warns 2018-04-04 20:24:09 -07:00
eeae0ddab4 start client timeout for response only 2018-04-04 20:15:47 -07:00
c1af59c618 update juniper example 2018-04-04 17:57:02 -07:00
d8a9606162 add connection limits to pool 2018-04-04 16:39:01 -07:00
8038a52287 run test coverage on beta 2018-04-04 08:12:33 -07:00
c273b7ac3f update json example 2018-04-04 08:08:31 -07:00
df21892b5b added extractor configuration 2018-04-03 22:06:18 -07:00
a255a6fb69 use build_response method 2018-04-03 17:37:17 -07:00
b693d5491b Merge pull request #157 from krircc/master
only use diesel::r2d2 feature. no need r2d2_diesel create
2018-04-03 08:18:16 -07:00
56a31ea0ee only use diesel::r2d2 feature. no need r2d2_diesel create 2018-04-03 22:37:53 +08:00
2a269f1111 update changes 2018-04-02 22:08:04 -07:00
fee30d6f47 fix doc test compatibility 2018-04-02 22:01:20 -07:00
476b1fb36a simplify DefaultHeaders middleware 2018-04-02 21:43:50 -07:00
3b93bff602 add ErrorHandlers middleware 2018-04-02 21:37:00 -07:00
d292c5023f add String and Bytes extractor 2018-04-02 16:19:18 -07:00
ef6f310060 update urlencoded example in guide 2018-04-02 15:08:49 -07:00
a6cbdde43f add extractor for Binary type; move all extractors to separate module 2018-04-02 14:55:42 -07:00
cbf4c61eb5 add urlencoded body extractor 2018-04-02 14:00:18 -07:00
280c8d87f8 expose ResourceType 2018-04-02 11:18:31 -07:00
83bf852192 Fix logger request duration calculation 2018-04-02 11:09:24 -07:00
9d39f441e9 Merge pull request #153 from rofrol/add-header-for-juniper-example
Add header for juniper example
2018-04-02 10:51:42 -07:00
03d851680b Merge branch 'master' into add-header-for-juniper-example 2018-04-02 10:51:35 -07:00
0ddd018214 Merge pull request #154 from dholbert/patch-1
Use https (not http) url for meritbadge
2018-04-02 10:50:17 -07:00
8219a7aebe Use https (not http) url for meritbadge
Right now this readme file uses an HTTP url to reference a meritbadge image, which ends up producing "broken https" UI on the crates.io page https://crates.io/crates/actix-web. This patch just upgrades this to an HTTPS url (which still works), to avoid that problem. (Literally a 1-character change, changing "http" to "https" in "http://meritbadge.herokuapp.com/actix-web")
2018-04-02 10:44:46 -07:00
6c906b08e1 match resource path before executing middlewares 2018-04-02 10:27:37 -07:00
220cbe40e5 Add header for juniper example 2018-04-02 19:10:33 +02:00
74d0656d27 update diesel example 2018-04-01 18:24:07 -07:00
17c27ef42d HttpRequest::resource() returns current matched resource 2018-04-01 17:37:22 -07:00
b2e771df2c use r2d2 for diesel example 2018-04-01 08:20:15 -07:00
a5a36ff194 update readme example 2018-03-31 17:15:44 -07:00
97e2bcd055 allow primitive types for Path extractor 2018-03-31 17:12:08 -07:00
23cfa649f4 update tests 2018-03-31 10:21:54 -07:00
8791c0f880 simplify With handlers 2018-03-31 09:58:33 -07:00
16c212f853 add extractors info to guide 2018-03-31 09:18:25 -07:00
3ee228005d rename Application 2018-03-31 00:16:55 -07:00
7a743fa6b5 update examples 2018-03-30 23:37:15 -07:00
44e3df82f6 simplify http response construction; deprecate httpcodes 2018-03-30 23:07:33 -07:00
8d8f6bedad update examples 2018-03-30 18:54:38 -07:00
9e751de707 re-arrange modules and exports 2018-03-30 17:31:18 -07:00
b16419348e add from HttpRequest to a HttpRequestBuilder 2018-03-30 14:30:24 -07:00
3ccaa04575 unhide AsyncResponder; remove unused code 2018-03-30 09:34:03 -07:00
d80b84c915 add test builder guide information 2018-03-29 19:23:45 -07:00
145010a2b0 use unreachable instead of panic 2018-03-29 15:55:27 -07:00
3e98177fad added State extractor 2018-03-29 15:41:13 -07:00
d24752d9bc update example in readme 2018-03-29 15:07:12 -07:00
92fe2e96de update doc strings 2018-03-29 15:00:18 -07:00
3cf54bc0fd proper serde deserializer implementation for path 2018-03-29 14:30:45 -07:00
86dd732704 use FromRequest instead of HttpRequestExtractor 2018-03-29 13:12:28 -07:00
dfd8f1058e move NormalizePath type to separate module 2018-03-29 11:39:21 -07:00
f5636f321b drop deprecated code 2018-03-29 11:06:44 -07:00
ae6c9cb7fa re-arrange exports, some doc string updates 2018-03-29 10:44:26 -07:00
32052c2750 update guide 2018-03-29 10:01:07 -07:00
7d6deab9fb drop request's extract_xxx methods 2018-03-29 09:26:01 -07:00
9e61c67128 do not re-export Version 2018-03-28 22:00:36 -07:00
13bb5f20d2 fix export name 2018-03-28 21:58:08 -07:00
d14991ec96 update doc strings 2018-03-28 21:49:50 -07:00
45dec8d0c0 optimize with and with2 method impls and tests 2018-03-28 21:33:40 -07:00
90e3aaaf8a fix router cannot parse Non-ASCII characters in URL #137 2018-03-28 16:10:58 -07:00
4f7d45ee9c remove unneeded import 2018-03-28 14:38:01 -07:00
e1d2536d85 remove unused code 2018-03-28 14:34:17 -07:00
65700281e8 add support for multiple extractors 2018-03-28 14:24:32 -07:00
80f6b93714 Merge pull request #138 from bwasty/guide
Guide: improve wording & style
2018-03-28 13:40:05 -07:00
5585465859 Merge branch 'master' into guide 2018-03-28 13:39:59 -07:00
368103dd09 guide: improve wording & style 2018-03-28 22:16:01 +02:00
df7ffe14f2 add PathAndQuery extractor 2018-03-28 11:20:06 -07:00
36161aba99 update Path and Query doc strings 2018-03-28 07:27:06 -07:00
9f5a91ae3c export types 2018-03-27 21:59:55 -07:00
4e61e0db34 mdbook 2018-03-27 21:33:35 -07:00
2dfccdd924 allow to fail nightly 2018-03-27 20:57:02 -07:00
4358da9926 refactor WithHandler trait 2018-03-27 20:33:24 -07:00
62fb75ff95 add Application::configure method, it simplifies configuration process 2018-03-27 11:16:02 -07:00
29a0feb415 fix guide example 2018-03-27 07:47:29 -07:00
dcc5eb7ace pass request as value 2018-03-26 23:34:31 -07:00
81f4e12a27 fix doc string test 2018-03-26 23:29:53 -07:00
2f60a4b89d add handler with exatractor 2018-03-26 23:10:31 -07:00
b03c7051ff add extractor info to the guide 2018-03-26 18:35:08 -07:00
8fff2c7595 remove Path and Query from public api 2018-03-26 18:18:38 -07:00
052d5f0bc5 silence AsciiExt deprecation warn 2018-03-26 16:12:25 -07:00
68cf32e848 add path and query extractors 2018-03-26 15:58:30 -07:00
a56e5113ee process transfer-encoding before content-length, fix tests on 32bit platform 2018-03-24 09:22:34 -07:00
5127b85672 Merge pull request #132 from andreevlex/spell-check-24-03
spelling check
2018-03-24 11:47:11 +03:00
2d80c5053d spelling check 2018-03-24 09:35:52 +03:00
d46854b315 bump version 2018-03-22 21:16:42 -07:00
47f836cd1b add helper method for response creation 2018-03-22 21:14:57 -07:00
449709dd7e add 0.5 sec deley before exit 2018-03-22 18:41:02 -07:00
5a25fd95f5 Fix panic on invalid URL characters #130 2018-03-22 18:08:12 -07:00
b942bcc4a6 Fix long client urls #129 2018-03-22 07:44:16 -07:00
1107fdec9d fix guide 2018-03-21 21:02:57 -07:00
04515e4697 update guide 2018-03-21 21:02:04 -07:00
93d99b5a49 Use more ergonomic actix_web::Error instead of http::Error for ClientRequestBuilder::body() 2018-03-21 20:19:31 -07:00
e49910cdab Use more ergonomic actix_web::Error instead of http::Error for HttpResponseBuilder::body() 2018-03-21 20:15:52 -07:00
e8a1850c79 add helper conversion from ClientResponse for HttpResponseBuilder 2018-03-21 20:04:35 -07:00
afb81b6b8f add convinience ClientRequest::build_from() from HttpRequest 2018-03-21 19:54:21 -07:00
4866a26578 make streaming method more ergonomic 2018-03-21 19:14:18 -07:00
2d75ced4ed fix client connection pooling 2018-03-21 11:51:08 -07:00
7bcc258b09 Use fast compression setting 2018-03-21 08:56:21 -07:00
d5fa0a9418 disable brotli if feature is not enabled, faster compression 2018-03-21 08:03:21 -07:00
ce6d237cc1 prepare 0.4.10 release 2018-03-20 15:53:39 -07:00
70caa2552b simplify httpresponse release 2018-03-20 15:51:19 -07:00
ee7d58dd7f disable h2 2018-03-20 12:35:44 -07:00
c4f4cadb43 Fix http/2 date header generation 2018-03-20 11:40:05 -07:00
978091cedb wake up io task when next chunk of data is needed 2018-03-20 11:37:13 -07:00
8198f5e10a Refactor TestServer configuration 2018-03-20 11:23:35 -07:00
6cd40df387 Fix server websockets big payloads support 2018-03-19 17:27:03 -07:00
35ee5d36d8 actix 0.5.5, ws test 2018-03-19 13:12:36 -07:00
e7ec0f9fd7 ws tests and proper write payload ref 2018-03-19 09:30:58 -07:00
f4a47ef71e allow set client request/ws timeout 2018-03-18 19:27:51 -07:00
6b1a79fab8 update example 2018-03-18 16:27:34 -07:00
ab73da4a1a use Error instead of InternalError for helper methods error::ErrorXXX 2018-03-18 14:18:47 -07:00
e0c8da567c various optimizations 2018-03-18 11:05:44 -07:00
c10dedf7e4 Merge pull request #124 from DoumanAsh/show_hidden
Show Request's hidden methods
2018-03-17 18:39:21 +03:00
ec192e0ab1 Show Request's hidden methods 2018-03-17 18:10:22 +03:00
6d792d9948 simplify h1 parse 2018-03-16 20:56:23 -07:00
1fe4315c94 use actix 0.5.4 2018-03-16 13:37:47 -07:00
381b90e9a1 bump version 2018-03-16 12:31:29 -07:00
2d18dba40a fix compilation 2018-03-16 12:28:08 -07:00
d2693d58a8 clippy warnings 2018-03-16 12:12:55 -07:00
84bf282c17 add basic client connection pooling 2018-03-16 12:04:01 -07:00
b15b5e5246 check number of available workers 2018-03-16 11:17:27 -07:00
52b3b0c362 Merge pull request #119 from DoumanAsh/default_static_files
Add default resource for StaticFiles
2018-03-16 20:12:07 +03:00
64c4cefa8f Merge branch 'master' into default_static_files 2018-03-16 09:31:36 -07:00
7e8b231f57 disable test 2018-03-16 09:13:36 -07:00
8a344d0c94 Add default resource for StaticFiles 2018-03-16 19:04:36 +03:00
4096089a3f allow to disable http/2 support 2018-03-16 08:48:44 -07:00
b16f2d5f05 proper check for actor context poll 2018-03-16 08:04:26 -07:00
5baf15822a always start actors 2018-03-16 07:46:27 -07:00
5368ce823e Merge pull request #123 from h416/patch-1
fix typo
2018-03-16 05:31:10 -07:00
4effdf065b fix typo 2018-03-16 19:03:16 +09:00
61970ab190 always poll stream or actor for the first time 2018-03-15 17:11:49 -07:00
484b00a0f9 Merge branch 'master' of github.com:actix/actix-web 2018-03-15 16:55:33 -07:00
73bf2068aa allow to use NamedFile with any request method 2018-03-15 16:55:22 -07:00
1cda949204 Merge pull request #122 from mockersf/test_qp
test for query parameters in client
2018-03-14 16:10:31 -07:00
ad6b823255 test for query parameters in client 2018-03-14 21:45:49 +01:00
0f064db31d Move brotli encoding to a feature 2018-03-13 17:21:22 -07:00
fd0bb54469 add debug formatter for ClientRequestBuilder 2018-03-13 15:09:05 -07:00
e27bbaa55c Update CHANGES.md 2018-03-13 13:15:21 -07:00
8a50eae1e2 Merge pull request #121 from glademiller/master
Send Query Parameters in client requests
2018-03-13 13:14:51 -07:00
38080f67b3 If no path is available from the URI request / 2018-03-13 13:35:11 -06:00
08504e0892 Move path call inline into write 2018-03-13 13:26:13 -06:00
401c0ad809 https://github.com/actix/actix-web/issues/120 - Send Query Parameters in client requests 2018-03-13 13:17:55 -06:00
b4b0deb7fa Wake payload reading task when data is available 2018-03-12 16:29:13 -07:00
05ff35d383 Fix server keep-alive handling 2018-03-12 16:16:17 -07:00
29c3e8f7ea update test 2018-03-12 10:19:09 -07:00
6657446433 Allow to set read buffer capacity for server request 2018-03-12 10:01:56 -07:00
46b9a9c887 update readme 2018-03-12 09:13:04 -07:00
b3cdb472d0 remove reserved state for h2 write if buffer is empty 2018-03-12 09:04:54 -07:00
31e1aab9a4 do not log WouldBlock error from socket accept 2018-03-12 09:02:15 -07:00
67f383f346 typo 2018-03-11 16:53:46 -07:00
49f5c335f6 better sleep on error 2018-03-11 16:52:20 -07:00
692e11a584 bump version 2018-03-11 16:40:25 -07:00
208117ca6f Merge pull request #118 from messense/feature/sockets-vec
Use Vec instead of HashMap to store sockets in HttpServer
2018-03-11 16:38:23 -07:00
3e276ac921 Merge branch 'master' into feature/sockets-vec 2018-03-11 16:38:17 -07:00
4af115a19c Fix steraming response handling for http/2 2018-03-11 16:37:44 -07:00
051703eb2c Fix connection get closed too early 2018-03-11 15:37:33 -07:00
31fbbd3168 Fix panic on unknown content encoding 2018-03-11 14:50:13 -07:00
fee1e255ac add comments 2018-03-11 10:10:30 -07:00
a4c933e56e update doc string 2018-03-11 09:36:54 -07:00
9ddf5a3550 better doc string for Either 2018-03-11 09:28:22 -07:00
9ab0fa604d Use Vec instead of HashMap to store sockets in HttpServer 2018-03-11 17:29:44 +08:00
6c709b33cc return error on write zero bytes 2018-03-10 10:42:46 -08:00
71b4c07ea4 Fix json content type detection 2018-03-10 10:27:29 -08:00
ac9eba8261 add api doc for Either 2018-03-10 10:12:44 -08:00
cad55f9c80 add Either responder 2018-03-10 09:39:43 -08:00
4263574a58 fix panic in cors if request does not contain origin header and send_wildcard is not set 2018-03-10 08:31:20 -08:00
84ef5ee410 Merge pull request #116 from messense/feature/from-usize-to-keepalive
Impl From<usize> and From<Option<usize>> for KeepAlive
2018-03-10 22:55:55 +08:00
598fb9190d rerun build if USE_SKEPTIC env var changed 2018-03-10 17:53:11 +08:00
9a404a0c03 Impl From<usize> and From<Option<usize>> for KeepAlive 2018-03-10 17:52:50 +08:00
3dd8fdf450 fix guide 2018-03-09 21:40:51 -08:00
05f5ba0084 refactor keep-alive; fixed write to socket for upgraded connection 2018-03-09 16:21:14 -08:00
8169149554 update wstool 2018-03-09 13:12:25 -08:00
8d1de6c497 ws client timeouts 2018-03-09 13:12:14 -08:00
caaace82e3 export symbols 2018-03-09 13:03:15 -08:00
02dd5375a9 aling mask to 8 bytes 2018-03-09 10:25:47 -08:00
717602472a clippy warnings 2018-03-09 10:11:38 -08:00
b56be8e571 write buffer capacity for client 2018-03-09 10:09:13 -08:00
2853086463 add write buffer capacity config 2018-03-09 10:00:15 -08:00
e2107ec6f4 use small vec on hot path 2018-03-09 08:00:44 -08:00
c33caddf57 update tests 2018-03-09 05:50:47 -08:00
db1e04e418 prepare release 2018-03-09 05:42:42 -08:00
f8b8fe3865 add space to cookie header 2018-03-09 05:38:07 -08:00
1c6ddfd34c naming 2018-03-09 05:36:40 -08:00
49e007ff2a move protobuf support to the example 2018-03-09 05:29:06 -08:00
2068eee669 update readme 2018-03-08 20:58:05 -08:00
f3c63e631a add protobuf feature 2018-03-08 20:56:18 -08:00
3f0803a7d3 Merge branch 'master' of github.com:actix/actix-web 2018-03-08 20:39:10 -08:00
f12b613211 more ws optimizations 2018-03-08 20:39:05 -08:00
695c052c58 Merge pull request #115 from kingxsp/master
Add protobuf support
2018-03-08 18:59:18 -08:00
63634be542 Merge branch 'master' into master 2018-03-09 10:22:15 +08:00
f88f1c65b6 update tests 2018-03-08 18:19:46 -08:00
a0b589eb96 Add protobuf support 2018-03-09 10:05:13 +08:00
ebdc983dfe optimize websocket stream 2018-03-08 17:19:50 -08:00
395243a539 another attempt to fix cookie handling 2018-03-08 11:16:54 -08:00
1ab676d7eb bump version and add some tests 2018-03-07 22:40:46 -08:00
47f01e5b7e update doc string 2018-03-07 21:39:20 -08:00
ffb89935b6 update all features 2018-03-07 21:37:42 -08:00
77a111b95c prepare release 2018-03-07 21:28:54 -08:00
6c0fb3a7d2 handle panics in worker threads 2018-03-07 21:10:53 -08:00
824244622f update test 2018-03-07 17:42:57 -08:00
42d2a29b1d non-blocking processing for NamedFile 2018-03-07 17:40:13 -08:00
af8875f6ab sleep on accept socket error 2018-03-07 15:52:05 -08:00
1db1ce1ca3 one more cookie handling fix 2018-03-07 15:41:46 -08:00
f55ef3a059 create default CpuPool 2018-03-07 14:56:53 -08:00
67bf0ae79f fix HttpServer::listen method 2018-03-07 14:46:12 -08:00
b06cf32329 Merge pull request #114 from DancingBard/master
BoyScoutRule: Fixed typo
2018-03-07 13:07:10 -08:00
7cce29b633 BoyScoutRule: Fixed typo 2018-03-07 21:54:25 +01:00
c26d9545a5 map connector timeout error 2018-03-07 12:09:53 -08:00
b950d6997d add csrf link to readme 2018-03-07 11:31:02 -08:00
0bf29a522b Allow to use std::net::TcpListener for HttpServer 2018-03-07 11:28:44 -08:00
24342fb745 Merge pull request #113 from niklasf/csrf-upgrade
Let CSRF filter catch cross-site upgrades
2018-03-07 09:58:30 -08:00
0278e364ec add tests for csrf upgrade filter 2018-03-07 18:42:21 +01:00
b9d6bbd357 filter cross-site upgrades in csrf middleware 2018-03-07 17:49:30 +01:00
5816ecd1bc fix variable name: cors -> csrf 2018-03-07 17:44:19 +01:00
2ff55ee1c5 Update CHANGES.md 2018-03-07 06:14:44 -08:00
b42de6c41f Merge pull request #111 from adwhit/cookie-handling
Fix client cookie handling
2018-03-07 06:13:02 -08:00
9e0e081c90 Merge branch 'master' into cookie-handling 2018-03-07 06:12:37 -08:00
178f5a104e Merge pull request #110 from messense/feature/tools-actix
Use actix from crates.io in tools/wsload
2018-03-07 06:10:18 -08:00
1e42f9575a Merge branch 'master' into feature/tools-actix 2018-03-07 06:09:09 -08:00
24dfcf1303 Merge pull request #109 from kingoflolz/master
make session an optional feature
2018-03-07 06:04:55 -08:00
6cc3aaef1b add client cookie handling test 2018-03-07 11:43:55 +00:00
436a16a2c8 Use actix from crates.io in tools/wsload 2018-03-07 19:26:23 +08:00
9afad5885b fix client cookie handling 2018-03-07 09:48:34 +00:00
04d0abb3c7 make session an optional feature 2018-03-07 15:38:58 +08:00
1e5daa1de8 update changes 2018-03-06 23:04:18 -08:00
d3c859f9f3 bump version 2018-03-06 22:44:06 -08:00
c1419413aa Fix client cookie support 2018-03-06 22:36:34 -08:00
acd33cccbb add tls 2018-03-06 17:34:46 -08:00
57a1d68f89 add client response timeout 2018-03-06 17:04:48 -08:00
5c88441cd7 Merge pull request #108 from glademiller/feature/allow_connection_timeout_to_be_set
Allow connection timeout to be set
2018-03-06 15:18:31 -08:00
6a3c5c4ce0 Merge branch 'master' into feature/allow_connection_timeout_to_be_set 2018-03-06 15:18:25 -08:00
14a511bdad use IntoHeaderValue and Header for client request 2018-03-06 15:18:04 -08:00
e4ed53d691 Merge branch 'feature/allow_connection_timeout_to_be_set' of https://github.com/glademiller/actix-web into feature/allow_connection_timeout_to_be_set 2018-03-06 15:44:18 -07:00
5bf4f3be8b Actix dependency needs to be updated to master 2018-03-06 15:43:56 -07:00
6b9e51740b Merge branch 'master' into feature/allow_connection_timeout_to_be_set 2018-03-06 15:28:31 -07:00
be7e8d159b Allow connection timeout to be set 2018-03-06 15:26:09 -07:00
ceb97cd6b9 Merge pull request #106 from niklasf/starting-url
give a url in the log when starting
2018-03-06 11:41:42 -08:00
85b650048d give a url in the log when starting 2018-03-06 20:37:18 +01:00
a0e6313d56 Fix compression #103 and #104 2018-03-06 11:02:03 -08:00
9cc6f6b1e4 Merge pull request #102 from mockersf/gzip
add tests with large random bodies for gzip
2018-03-06 08:48:06 -08:00
526753ee88 update tests for stable compiler 2018-03-06 07:56:43 -08:00
779e773185 add tests with large random bodies for gzip 2018-03-06 14:26:48 +01:00
7eb310f8ce fix guide 2018-03-06 00:44:45 -08:00
d573cf2d97 Merge branch 'master' of github.com:actix/actix-web 2018-03-06 00:43:34 -08:00
32b5544ad9 port hyper header 2018-03-06 00:43:25 -08:00
e182ed33b1 add Header trait 2018-03-05 19:28:42 -08:00
6f1836f80e Merge pull request #98 from flip111/patch-2
Update qs_14.md
2018-03-05 16:48:47 -08:00
5b530f11b5 Update qs_14.md
fix missing semicolon
2018-03-06 01:46:16 +01:00
0c30057c8c move headers to separate module; allow custom HeaderValue conversion 2018-03-05 16:45:54 -08:00
6078344ecc Merge pull request #97 from flip111/patch-1
Update qs_14.md
2018-03-05 16:36:32 -08:00
67f5a949a4 Update qs_14.md
fix syntax error on use statement
2018-03-06 01:35:41 +01:00
05e49e893e allow only GET and HEAD for NamedFile 2018-03-05 14:04:30 -08:00
c8844425ad Enable compression support for NamedFile 2018-03-05 13:31:30 -08:00
b282ec106e Add ResponseError impl for SendRequestError 2018-03-05 13:02:31 -08:00
ea2a8f6908 add http proxy example 2018-03-05 11:12:19 -08:00
2b942ec5f2 add uds example readme 2018-03-05 09:47:17 -08:00
bf77be0337 Merge pull request #95 from messense/feature/unix-socket-example
Add unix domain socket example
2018-03-05 09:37:00 -08:00
c2741054bb Add unix domain socket example 2018-03-05 22:14:25 +08:00
e708f51156 prep release 2018-03-04 20:28:06 -08:00
cbb821148b explicitly set tcp nodelay 2018-03-04 20:14:58 -08:00
d6b021e185 Merge pull request #94 from messense/feature/str-repeat
Use str::repeat
2018-03-04 19:57:49 -08:00
0adb7e8553 Use str::repeat 2018-03-05 09:54:58 +08:00
dbfa1f0ac8 fix example 2018-03-04 10:44:41 -08:00
11347e3c7d Allow to use Arc<Vec<u8>> as response/request body 2018-03-04 10:33:18 -08:00
631fe72a46 websockets text() is more generic 2018-03-04 10:18:42 -08:00
f673dba759 Fix handling of requests with an encoded body with a length > 8192 #93 2018-03-04 09:48:34 -08:00
ab978a18ff unix only test 2018-03-03 18:50:00 -08:00
327df159c6 prepare release 2018-03-03 18:46:22 -08:00
2ccbd5fa18 fix socket polling 2018-03-03 12:17:26 -08:00
058630d041 simplify channels list management 2018-03-03 11:16:55 -08:00
f456be0309 simplify linked nodes 2018-03-03 10:06:13 -08:00
9bd6cb03ac Merge branch 'master' of github.com:actix/actix-web 2018-03-03 09:29:46 -08:00
16afeda79c update changes 2018-03-03 09:29:36 -08:00
83fcdfd91f fix potential bug in payload processing 2018-03-03 09:27:54 -08:00
8f94ae41cc Merge pull request #90 from rvlzzr/master
move reuse_address before bind
2018-03-02 23:08:33 -08:00
4e41347de8 move reuse_address before bind 2018-03-02 22:57:11 -08:00
6acb6dd4e7 set release date 2018-03-02 22:31:58 -08:00
791a980e2d update tests 2018-03-02 22:08:56 -08:00
c2d8abcee7 Fix disconnect on idle connections 2018-03-02 20:47:23 -08:00
16c05f07ba make HttpRequest::match_info_mut() public 2018-03-02 20:40:08 -08:00
2158ad29ee add Pattern::with_prefix, make it usable outside of actix 2018-03-02 20:39:22 -08:00
feba5aeffd bump version 2018-03-02 14:31:23 -08:00
343888017e Update CHANGES.md 2018-03-02 12:26:31 -08:00
3a5d445b2f Merge pull request #89 from niklasf/csrf-middleware
add csrf filter middleware
2018-03-02 12:25:23 -08:00
e60acb7607 Merge branch 'master' into csrf-middleware 2018-03-02 12:25:05 -08:00
bebfc6c9b5 sleep for test 2018-03-02 11:32:37 -08:00
3b2928a391 Better naming for websockets implementation 2018-03-02 11:29:55 -08:00
10f57dac31 add csrf filter middleware 2018-03-02 20:13:43 +01:00
b640b49b05 adjust low buf size 2018-03-01 20:13:50 -08:00
1fea4bd9a6 prepare release 2018-03-01 20:01:25 -08:00
206c4e581a rename httpcodes 2018-03-01 19:12:59 -08:00
4e13505b92 rename .p to a .filter 2018-03-01 18:42:50 -08:00
5b6d7cddbf Fix payload parse in situation when socket data is not ready 2018-03-01 18:27:04 -08:00
4aaf9f08f8 update readme 2018-02-28 22:31:54 -08:00
b0ba23ff55 Merge pull request #88 from rofrol/patch-2
be consistent with host - had CORS preflight once
2018-02-28 17:07:57 -08:00
42b19b1819 Merge branch 'master' into patch-2 2018-02-28 17:07:44 -08:00
0335fde3f9 Update README.md 2018-02-28 16:58:05 -08:00
f27edbff89 be consistent with host - had CORS preflight once 2018-03-01 01:01:27 +01:00
d62d6e68e0 use new version of http crate 2018-02-28 14:16:55 -08:00
1284264511 Update CHANGES.md 2018-02-28 12:35:16 -08:00
d977fe563b Merge pull request #87 from adwhit/fix-session-set
fix session mut borrow lifetime
2018-02-28 12:34:46 -08:00
bb68f9dd90 add session borrow fix to changes 2018-02-28 19:52:53 +00:00
313396d9b5 fix session mut borrow lifetime 2018-02-28 19:35:26 +00:00
171a23561e export Drain 2018-02-28 11:10:54 -08:00
67f33a4760 add redis session example 2018-02-28 10:26:40 -08:00
764421fe44 update categories 2018-02-27 23:51:57 -08:00
b339ea0a3a update versions in guide 2018-02-27 23:31:43 -08:00
8994732227 doc strings 2018-02-27 23:30:26 -08:00
7591592279 fix handle big data chunkd for parsing 2018-02-27 23:04:57 -08:00
4a48b43927 big value 2018-02-27 21:49:08 -08:00
b1ad4763a2 check juniper example 2018-02-27 21:23:41 -08:00
2f3a2115c0 Merge pull request #86 from pyros2097/master
add juniper example
2018-02-27 21:21:52 -08:00
1283c00583 add juniper example 2018-02-28 10:41:24 +05:30
9f81eae215 build docs on nightly 2018-02-27 21:04:22 -08:00
ccb6ebb259 headers test 2018-02-27 20:49:53 -08:00
da76de76f0 upgrade sha crate 2018-02-27 20:32:51 -08:00
c316a99746 stop server test 2018-02-27 20:04:01 -08:00
1e2aa4fc90 mark context as modified after writing data 2018-02-27 18:05:06 -08:00
e2c8f17c2c drop connection if handler get dropped without consuming payload 2018-02-27 16:08:57 -08:00
9b06eac720 Merge branch 'master' of github.com:actix/actix-web 2018-02-27 15:41:53 -08:00
4f99cd1580 add ws error tracing 2018-02-27 15:38:57 -08:00
f56fa49a9b Merge pull request #84 from mpaltun/patch-1
Fix typos in README
2018-02-27 15:18:16 -08:00
1f063e4136 move with_connector method to ClientRequestBuilder 2018-02-27 15:14:33 -08:00
33c935dccc Fix typos in README 2018-02-28 01:13:59 +02:00
a7bf635158 unify headers and body processing for client response and server request 2018-02-27 15:03:28 -08:00
aac9b5a97c update readme 2018-02-27 12:49:11 -08:00
6c480fae90 added HttpRequest::encoding() method; fix urlencoded parsing with charset 2018-02-27 11:31:54 -08:00
5dcb558f50 refactor websockets handling 2018-02-27 10:09:24 -08:00
a344c3a02e remove read buffer management api 2018-02-26 20:07:22 -08:00
0ab8bc11f3 fix guide example 2018-02-26 16:41:57 -08:00
abae65a49e remove unused code 2018-02-26 16:11:00 -08:00
d6fd4a3524 use buffer capacity; remove unused imports 2018-02-26 15:34:25 -08:00
72aa2d9eae clippy warnings 2018-02-26 14:33:56 -08:00
644f1a9518 refactor ws frame parser 2018-02-26 13:58:23 -08:00
56ae565688 fix guide examples 2018-02-26 08:02:58 -08:00
0a3b776aa7 refactor multipart stream 2018-02-26 06:00:54 +03:00
6ef9c60361 add Read and AsyncRead impl to HttpRequest 2018-02-25 21:26:58 +03:00
a2b98b31e8 refactor payload related futures for HttpRequest 2018-02-25 20:34:26 +03:00
ab5ed27bf1 refactor and simplify content encoding 2018-02-25 11:43:00 +03:00
141b992450 Make payload and httprequest a stream 2018-02-25 11:21:45 +03:00
4e41e13baf refactor client payload processing 2018-02-25 11:18:17 +03:00
ea8e8e75a2 fix websocket example 2018-02-24 08:41:58 +03:00
a855c8b2c9 better ergonomics for WsClient::client() 2018-02-24 08:14:21 +03:00
fd31eb74c5 better ergonomics for ws client 2018-02-24 07:36:50 +03:00
3b22b1b168 Merge pull request #78 from pyros2097/master
Fix websocket example path
2018-02-23 01:47:40 -08:00
7a7df7f8fb Merge branch 'master' into master 2018-02-23 01:47:30 -08:00
25aabfb3e2 fix big ws frames 2018-02-23 10:45:33 +01:00
3a3657cfaf Update qs_9.md 2018-02-23 12:39:19 +05:30
aff43cc8b8 fix routes registration order 2018-02-22 05:48:18 -08:00
4a9c1ae894 allow to use Connection for sending client request 2018-02-21 22:53:23 -08:00
4a07430e8e remove RegexSet mention 2018-02-21 22:04:59 -08:00
9a076c69d1 update route matching guide section 2018-02-21 22:00:22 -08:00
8f2d3a0a76 fix NormalizePath helper 2018-02-21 14:53:42 -08:00
d4611f8bb9 Merge branch 'master' of github.com:actix/actix-web 2018-02-21 14:31:31 -08:00
fd56e5dc82 do not use regset for route recognition 2018-02-21 14:31:22 -08:00
7c74259453 Merge pull request #77 from rofrol/patch-1
could used -> could be used, latest actix sync
2018-02-21 10:00:08 -08:00
6a01af32bc could used -> could be used, latest actix sync 2018-02-21 18:59:00 +01:00
5634e5794f more tests for NormalizePath helper 2018-02-20 13:03:21 -08:00
187644e178 update logger doc string 2018-02-20 12:53:51 -08:00
7198dde465 add logger info 2018-02-20 12:49:42 -08:00
2374aa42ed set date header for client requests 2018-02-19 23:18:18 -08:00
03912d2089 support client request's async body 2018-02-19 22:48:27 -08:00
3f95cce9e8 allow to pass different binary data 2018-02-19 20:03:57 -08:00
979cea03ac added TestRequest::set_payload() 2018-02-19 20:01:38 -08:00
6424defee6 code coverage on 1.21 2018-02-19 17:32:22 -08:00
6ee14efbe2 optimize http message serialization 2018-02-19 17:21:04 -08:00
4d81186059 escape router pattern re 2018-02-19 14:57:57 -08:00
ddc82395e8 try to remove trailing slash for normalize path handler 2018-02-19 14:27:36 -08:00
360ffbba68 clone router with httprequest 2018-02-19 14:26:51 -08:00
f2f1798215 allow to send request using custom connector 2018-02-19 13:41:21 -08:00
548f4e4d62 replace reqwest with actix::client 2018-02-19 13:18:18 -08:00
cb70d5ec3d refactor http client 2018-02-19 03:11:11 -08:00
edd114f6e4 allow to set default content encoding on application level 2018-02-18 22:23:17 -08:00
816c6fb0e0 log 5xx responses as error 2018-02-18 09:57:57 -08:00
0da382a7a4 use actix 0.5 release 2018-02-17 13:33:38 -08:00
3e3d3279b8 deregister server socket on shutdown 2018-02-16 09:42:15 -08:00
3c95823e53 add r2d2 example 2018-02-15 23:05:10 -08:00
8607c51bcf do not stop accept thread on error 2018-02-15 22:02:03 -08:00
080bb3e5ae disable dead code link 2018-02-15 16:25:43 -08:00
d31e71a169 update examples 2018-02-15 13:59:25 -08:00
7b0e1642b6 add techempower benchmark link 2018-02-15 09:53:09 -08:00
096dee519c Merge pull request #71 from rbtcollins/patch-1
Wait for spawned thread
2018-02-13 14:58:11 -08:00
8bce3b9d10 Merge branch 'master' into patch-1 2018-02-13 14:57:59 -08:00
b28ecbcf0c Update qs_2.md 2018-02-14 10:37:12 +13:00
8f9ec5c23c fix doc test 2018-02-13 07:50:49 -08:00
96b87761d1 update examples 2018-02-12 23:13:06 -08:00
b1eec3131f use newer api 2018-02-12 22:56:47 -08:00
a544034c06 use Recipient 2018-02-12 22:09:31 -08:00
4b8181476c consistently use #[cause] and display causing errors (#73) 2018-02-12 23:55:44 -06:00
eb041de36d update examples 2018-02-12 19:15:39 -08:00
80285f2a32 fix doc test 2018-02-12 18:38:13 -08:00
de869ed879 Merge pull request #72 from rbtcollins/patch-2
Use AtomicUsize properly
2018-02-12 17:46:03 -08:00
7ccacb92ce update websocket-chat example 2018-02-12 17:42:10 -08:00
57655d8153 Use AtomicUsize properly
doing a read+write on an atomic int will lose updates from other threads.
2018-02-13 13:47:59 +13:00
335ca8ff33 use new actix api 2018-02-12 16:08:04 -08:00
720d8c36c1 update names 2018-02-12 12:45:08 -08:00
8c1b5fa945 sync with latest actix 2018-02-12 12:17:30 -08:00
232aba2080 Wait for spawned thread
A spawned thread doesn't block the main thread exiting unless explicitly joined.
The demo as written in the guide simply exits immediately at the moment.
2018-02-12 23:52:03 +13:00
30bdf9cb5e update actix api 2018-02-12 01:13:06 -08:00
285c66e7d8 build docs for apln and tls features 2018-02-10 11:39:12 -08:00
856055c6ca simplify HttpServer::start_tls() method 2018-02-10 11:34:54 -08:00
e3081306da update doc string 2018-02-10 11:29:40 -08:00
94c4053cb5 more HttpServer type simplification 2018-02-10 11:01:54 -08:00
762961b0f4 simplify HttpServer type definition 2018-02-10 10:22:03 -08:00
3109f9be62 special handling for upgraded pipeline 2018-02-10 00:05:20 -08:00
2d049e4a9f update example 2018-02-09 22:46:34 -08:00
0c98775b51 refactor h1 stream polling 2018-02-09 22:26:48 -08:00
b4b5c78b51 optimize ws frame generation 2018-02-09 20:43:14 -08:00
78da98a16d add wsload tool; optimize ws frame parser 2018-02-09 17:20:28 -08:00
74377ef73d fix back pressure for h1 import stream 2018-02-09 16:20:10 -08:00
728377a447 fix example 2018-02-08 20:55:34 -08:00
73ed1342eb more actix compatibility 2018-02-08 17:13:56 -08:00
bc6300be34 actix compatibility 2018-02-08 17:08:57 -08:00
2faf3a5eb6 fix deprecation warnings, update actix 2018-02-08 17:00:22 -08:00
6181a84d7b update websocket-chat example 2018-02-08 14:03:41 -08:00
f8f99ec0c7 Disable signals in HttpServers started by the tests. (#69)
Something is wrong with signals on windows.
This change causes the unit tests to pass on Windows.
2018-02-08 14:55:47 -06:00
bd03ba1192 Update most examples to use actix from git (#68) 2018-02-08 00:08:36 -06:00
d0cbf7cd25 upgrade trust-dns-resolver 2018-02-07 14:58:08 -08:00
93aa220e8d remove default impl for std error, it prevents use of Fail 2018-02-07 13:57:58 -08:00
81e4fb9353 Avoid using Path to calculate URIs, because it doesn't do the right thing on Windows (#67)
Redirecting to index files now always uses `/` instead of backslash on windows.
2018-02-07 15:31:09 -06:00
884ea02f5c Allow returning failure::Error from handlers (#65)
This implements From<failure::Error> for Error (by way of `failure::Compat`)
and ResponseError for failure::Compat<T>.
2018-02-06 10:26:50 -06:00
b6d5516e3a remove rust_backtrace for appveyor 2018-02-04 10:48:16 -08:00
46841cc87e update config for appveyor 2018-02-04 10:31:39 -08:00
7ad66956b2 add HttpRequest::uri_mut(), allows to modify request uri 2018-02-03 08:31:32 -08:00
d568161852 update websocket-chat example 2018-02-03 08:25:31 -08:00
671ab35cf6 re enable 1.21 2018-02-02 21:32:43 -08:00
c63ad4b6f1 appveyor cfg 2018-02-02 21:31:16 -08:00
eb713bd60e update actix version 2018-02-01 01:08:08 -08:00
2b74fbf586 fix websocket example 2018-01-31 13:18:30 -08:00
58f85658bd update actix 2018-01-31 12:57:02 -08:00
7e9fbfca72 missing http codes 2018-01-31 12:34:58 -08:00
5115384501 Merge pull request #64 from andreevlex/fix-2
spelling check
2018-01-31 10:54:05 -08:00
a1b96b1cf4 return "chnked" value 2018-01-31 21:37:12 +03:00
a565e71018 spelling check 2018-01-31 20:28:53 +03:00
e41b175e3d Update README.md 2018-01-31 06:40:00 -08:00
db39f122be Update README.md 2018-01-31 06:37:37 -08:00
afd2dc4666 Update README.md 2018-01-31 06:36:15 -08:00
cba7e426a5 Update README.md 2018-01-31 06:35:47 -08:00
01e7cc9620 Update README.md 2018-01-31 06:34:50 -08:00
5a5497b745 add close ws test 2018-01-30 16:04:04 -08:00
b698e3546b link to websocket example 2018-01-30 15:26:58 -08:00
e99a5e8144 drop local actix ref 2018-01-30 15:19:30 -08:00
577f91206c added support for websocket testing 2018-01-30 15:13:33 -08:00
76f9542df7 rename module 2018-01-30 13:04:52 -08:00
9739168d48 fix limit usage for Request/Response Body future 2018-01-30 12:44:14 -08:00
5cbaf3a1b8 add client ssl support 2018-01-30 11:17:17 -08:00
a02e0dfab6 initial work on client connector 2018-01-29 23:01:20 -08:00
5cc3bba5cc change ws client names 2018-01-29 15:45:37 -08:00
6e51573975 app veyor config 2018-01-29 14:51:34 -08:00
b686f39d0b complete impl for client request and response 2018-01-29 14:44:25 -08:00
6416a796c3 add ClientRequest and ClientRequestBuilder 2018-01-29 11:45:33 -08:00
b6a394a113 added StaticFiles::inex_file config 2018-01-29 03:23:45 -08:00
456fd1364a add handle method to contexts 2018-01-28 09:47:46 -08:00
f3cce6a04c update websocket example 2018-01-28 09:07:12 -08:00
9835a4537a update websocket example 2018-01-28 08:58:18 -08:00
715ec4ae2f update actix 2018-01-28 08:26:36 -08:00
55b2fb7f77 update example 2018-01-28 01:04:58 -08:00
7c7743c145 use right path 2018-01-27 22:52:17 -08:00
826fc62299 disable websocket-chat example 2018-01-27 22:44:50 -08:00
5dd2e7523d basic websocket client 2018-01-27 22:03:03 -08:00
4821d51167 fix actix compatibility 2018-01-27 11:15:03 -08:00
c446be48e3 min rust version 1.21 2018-01-27 10:58:09 -08:00
042f8391bb Merge branch 'master' of github.com:actix/actix-web 2018-01-27 10:05:07 -08:00
d4bc3294a3 actix compatibility 2018-01-27 10:04:56 -08:00
04d53d6f57 Merge pull request #59 from andreevlex/fix-cors
spelling check cors example
2018-01-26 21:42:36 -08:00
881e0e0346 spelling check 2018-01-27 08:38:17 +03:00
b9f8a00ba3 update cors example readme 2018-01-26 19:56:34 -08:00
99bed67bec rename cors example 2018-01-26 19:52:20 -08:00
52a454800f cleanup cors example 2018-01-26 19:51:13 -08:00
c09c8e4980 Merge pull request #58 from krircc/master
add actix-web-cors example
2018-01-26 19:43:40 -08:00
b931dda1fe Merge branch 'master' into master 2018-01-26 19:42:06 -08:00
74166b4834 add actix-web-cors example 2018-01-27 11:00:26 +08:00
4abb769ee5 fix request json loader; mime_type() method 2018-01-25 21:50:28 -08:00
e8e2ca1526 refactor alpn support; upgrade openssl to 0.10 2018-01-25 10:24:04 -08:00
78967dea13 stop http context immediately 2018-01-24 20:17:14 -08:00
58a5d493b7 re-eanble write backpressure for h1 connections 2018-01-24 20:12:49 -08:00
c5341017cd fix typo 2018-01-23 15:39:53 -08:00
f4873fcdee stop websocket context 2018-01-23 15:35:39 -08:00
35efd017bb impl waiting for HttpContext 2018-01-23 09:42:04 -08:00
fb76c490c6 mention tokio handle in guide 2018-01-22 20:10:05 -08:00
3653c78e92 check example on stable 2018-01-22 19:49:19 -08:00
1053c44326 pin new actix version 2018-01-22 17:01:54 -08:00
e6ea177181 impl WebsocketContext::waiting() method 2018-01-22 16:55:50 -08:00
1957469061 code of conduct 2018-01-21 15:29:02 -08:00
2227120ae0 exclude examples 2018-01-21 09:09:19 -08:00
21c8c0371d travis config 2018-01-21 08:50:29 -08:00
1914a6a0d8 Always enable content encoding if encoding explicitly selected 2018-01-21 08:31:46 -08:00
1cff4619e7 reduce threshold for content encoding 2018-01-21 08:12:32 -08:00
7bb7adf89c relax InternalError constraints 2018-01-20 22:02:42 -08:00
f55ff24925 fix guide example 2018-01-20 21:40:18 -08:00
f5f78d79e6 update doc strings 2018-01-20 21:16:31 -08:00
9180625dfd refactor helper error types 2018-01-20 21:11:46 -08:00
552320bae2 add error logging guide section 2018-01-20 20:21:01 -08:00
7cf221f767 Log request processing errors 2018-01-20 20:12:24 -08:00
98931a8623 test case for broken transfer encoding 2018-01-20 16:51:18 -08:00
ae10a89014 use ws masking from tungstenite project 2018-01-20 16:47:34 -08:00
71d534dadb CORS middleware: allowed_headers is defaulting to None #50 2018-01-20 16:36:57 -08:00
867bb1d409 Merge branch 'master' of github.com:actix/actix-web 2018-01-20 16:12:51 -08:00
91c44a1cf1 Fix HEAD requests handling 2018-01-20 16:12:38 -08:00
3bc60a8d5d Merge pull request #53 from andreevlex/spelling-check-2
spelling check
2018-01-16 12:07:58 -08:00
58df8fa4b9 spelling check 2018-01-16 21:59:33 +03:00
81f92b43e5 Merge pull request #52 from andreevlex/spelling-check
spelling check
2018-01-15 14:16:54 -08:00
e1d9c3803b spelling check 2018-01-16 00:47:25 +03:00
a7c24aace1 flush is useless 2018-01-14 19:28:34 -08:00
89a89e7b18 refactor shared bytes api 2018-01-14 17:00:28 -08:00
3425f7be40 fix tests 2018-01-14 14:58:58 -08:00
09a6f8a34f disable alpn feature 2018-01-14 14:44:32 -08:00
7060f298b4 use more binary 2018-01-14 14:40:39 -08:00
33dbe15760 use Binary for writer trait 2018-01-14 13:50:38 -08:00
e95c7dfc29 use local actix-web for examples 2018-01-13 19:04:07 -08:00
927a92fcac impl HttpHandler for Box<HttpHandler> and add helper method Application::boxed() #49 2018-01-13 18:58:17 -08:00
2b0f3d2a9a prepare release 2018-01-13 16:57:01 -08:00
93fdb596d4 Allow to explicitly disable chunked encoding 2018-01-13 16:17:33 -08:00
305666067e Do not enable chunked encoding for HTTP/1.0 2018-01-13 12:46:43 -08:00
b805d87ee7 no need for custom cookie module 2018-01-13 11:33:42 -08:00
bc6bb9984f user guide spelling 2018-01-13 11:17:48 -08:00
c043fd7912 Merge pull request #47 from belltoy/master
fix directory entry path
2018-01-13 11:16:53 -08:00
781282897a fix directory entry path 2018-01-13 08:37:27 +00:00
a9c71b2894 add link to cors middleware 2018-01-12 13:10:12 -08:00
edd26837dd update dependency specs in user guide 2018-01-12 12:54:57 -08:00
3105bca13b use cookie-rs released create 2018-01-12 12:32:54 -08:00
c470e7a02b use flate2 released crate 2018-01-12 12:31:33 -08:00
8a96e8fdd0 disable compression for static files 2018-01-11 23:49:53 -08:00
e919ec485e cleanup http channel 2018-01-11 22:06:06 -08:00
e482b88741 refactor http protocol selection procedure 2018-01-11 21:48:36 -08:00
eb8052b936 fix cors tests 2018-01-11 20:20:50 -08:00
dab918261c fix cors allowed header validation 2018-01-11 20:11:34 -08:00
11342e4566 add link to gitter 2018-01-11 18:49:30 -08:00
f7b895b53a add link to github 2018-01-11 18:47:34 -08:00
ac89880c0a move encoding to server 2018-01-11 18:41:33 -08:00
8a058efb4e move server protocol impl to submodule 2018-01-11 18:35:05 -08:00
fa93701bee upgrade packages 2018-01-11 16:47:55 -08:00
0707dfe5bb flush stream on drain 2018-01-11 16:22:27 -08:00
0a41ecd01d disable test 2018-01-11 15:38:57 -08:00
0648ad6f33 fix implicit chunked encoding 2018-01-11 15:26:46 -08:00
728d4f1f57 clean cargo before running skeptic tests 2018-01-11 11:39:17 -08:00
d152860fa7 add Cors::register method 2018-01-11 11:14:18 -08:00
43f14224b1 properly enable encoding tests 2018-01-10 22:42:26 -08:00
f7d9b45e64 travis config 2018-01-10 21:49:23 -08:00
448b73a4b5 encoding tests 2018-01-10 21:47:30 -08:00
1a31554ee6 travis config 2018-01-10 21:02:28 -08:00
49cdddf479 upgrade flate package 2018-01-10 20:28:06 -08:00
aed90ed458 rename payload 2018-01-10 20:08:13 -08:00
e0faf3f69c refactor pipeline impl 2018-01-10 16:45:57 -08:00
f7807e43d8 cleanup Binary type; more cors tests 2018-01-10 15:28:33 -08:00
fee54d1de0 tests for cors response 2018-01-10 14:56:45 -08:00
1445cc7a2c test for cors 2018-01-10 14:21:48 -08:00
16e9512457 better names for cors errors 2018-01-10 14:20:00 -08:00
615db0d9d8 complete cors implementation 2018-01-10 13:41:33 -08:00
3f3dcf413b move websocket code to submodule 2018-01-10 11:13:29 -08:00
d85081b64e update websocket examples 2018-01-10 10:40:14 -08:00
4b72a1b325 create custom WebsocketContext for websocket connection 2018-01-10 10:12:34 -08:00
8aae2daafa update example 2018-01-10 07:55:25 -08:00
d7f59ce481 add cors preflight request support 2018-01-09 23:55:42 -08:00
ce78f17a79 refactor Middleware trait, use Result 2018-01-09 22:48:35 -08:00
16310a5ebd initial work on cors middleware 2018-01-09 22:33:51 -08:00
e8412672a2 add resource level middlewares support 2018-01-09 20:00:18 -08:00
6c7dda495b add very simple http/2 test 2018-01-09 12:49:46 -08:00
584d0c9e99 Merge branch 'master' of github.com:actix/actix-web 2018-01-09 10:08:14 -08:00
a159a9cd6e cleanup doc tests 2018-01-09 10:08:06 -08:00
9d4e926302 Merge pull request #45 from ami44/master
fix url
2018-01-08 11:33:36 -08:00
41c94a1220 fix url 2018-01-08 19:10:47 +01:00
c7798ef45d update examples 2018-01-07 19:40:42 -08:00
d696c1692e Merge branch 'master' of github.com:actix/actix-web 2018-01-07 19:10:55 -08:00
f90bc0caae do no stop on write_eof 2018-01-07 19:10:42 -08:00
f802fe09e6 fix context poll 2018-01-07 17:13:49 -08:00
513fcd4a47 Update README.md 2018-01-07 08:33:06 -08:00
c28d052d22 Merge pull request #44 from krircc/master
IT's not true, actix-web are more features than other rust web frameworks
2018-01-07 08:32:08 -08:00
550f68ca05 Merge branch 'master' into master 2018-01-07 08:31:57 -08:00
3074071d03 Merge branch 'master' into master 2018-01-07 08:31:31 -08:00
3ffefb8b29 Merge pull request #42 from ami44/master
upd examples basics
2018-01-07 08:30:42 -08:00
3cf2fbbb23 Merge branch 'master' into master 2018-01-07 12:00:24 +01:00
896981cdf8 update examples 2018-01-06 23:22:10 -08:00
71da72efdb use general context impl 2018-01-06 22:59:39 -08:00
5e9b94a6dd Update README.md 2018-01-07 11:07:51 +08:00
665f4edf6b upd examples basics 2018-01-06 16:43:59 +01:00
247c23c1ea no need for StreamHandler 2018-01-06 01:06:35 -08:00
3ed9e872ad subscriber to os signals automatically 2018-01-05 16:32:36 -08:00
473ec38439 use dev cookies package as temp solution for ring problem 2018-01-05 14:50:33 -08:00
524493e0b0 pin nightly 2018-01-05 14:29:40 -08:00
5ae646332e update example to use actix 0.4 2018-01-05 14:01:19 -08:00
5ff35f5b99 upgrade to actix 0.4 2018-01-05 13:30:21 -08:00
dea354d6d8 fix basic example in guide 2018-01-04 16:21:18 -08:00
20d5c61c11 set nodelay for streams 2018-01-04 09:32:47 -08:00
91230afc44 fix time calculations 2018-01-04 09:32:33 -08:00
afeffe4b19 encode returns result 2018-01-04 09:32:15 -08:00
fdf7726831 update changelog 2018-01-03 23:59:12 -08:00
9559f6a175 introduce IoStream trait for low level stream operations 2018-01-03 23:41:55 -08:00
1f7aee23df shutdown io streams before exit 2018-01-03 22:43:44 -08:00
bf11bfed8e use explicit actix:: mod 2018-01-03 19:11:40 -08:00
e439d0546b * fix force_close
* shutdown io before exit

* fix response creation with body from pool
2018-01-03 18:21:34 -08:00
8348c830e2 no need for mut ref 2018-01-03 10:57:57 -08:00
ae084d1146 added helper future for reading request body 2018-01-03 09:23:58 -08:00
88031b7fde remove debug prints 2018-01-03 09:00:22 -08:00
70ea43b3c0 fix drain support for actor; make pattern more reusable 2018-01-02 23:43:17 -08:00
9e6d090fd0 update readme example 2018-01-02 19:57:25 -08:00
7af3b3f956 update example 2018-01-02 19:43:59 -08:00
3a59344ffb update h2 lib 2018-01-02 19:37:33 -08:00
3768a2885d fix examples 2018-01-02 15:52:11 -08:00
f0fdcc9936 handle application prefix for handlers; use handler for StaticFiles 2018-01-02 15:23:31 -08:00
77ba1de305 flush encoder 2018-01-02 14:53:51 -08:00
fb2c78d9fc add hello-world example 2018-01-02 13:42:30 -08:00
b49eadf7e5 fix content length serialization #33 2018-01-02 13:39:32 -08:00
9040f588af allow to handle entire sub path 2018-01-02 13:09:02 -08:00
284b59722a update websocket example 2018-01-01 09:31:42 -08:00
e798af26a2 Merge pull request #27 from ami44/master
move examples/websocket.rs to examples/websocket
2018-01-01 12:26:07 +01:00
fc88bb294a Merge remote-tracking branch 'upstream/master' 2018-01-01 12:22:03 +01:00
f3a90a2829 add example to workspace 2017-12-31 22:22:56 -08:00
d2f54b7d19 use workspace 2017-12-31 21:55:25 -08:00
8e89ff1d1e fix examples 2017-12-31 20:08:35 -08:00
cc38b30f7b refactor http actor usage 2017-12-31 17:26:32 -08:00
967d3244d7 fix http/2 support 2017-12-31 13:22:11 -08:00
d8548ad83b update examples/diesel readme 2017-12-31 08:12:26 +01:00
5741b8b372 add examples/websocket 2017-12-30 22:43:10 +01:00
7962d28a6d move examples/websocket.rs to examples/websocket 2017-12-30 21:47:39 +01:00
e18f9f3f3a Merge pull request #1 from actix/master
Update from original
2017-12-30 21:27:48 +01:00
73e2773a10 minor fix guide/ 2017-12-30 21:13:23 +01:00
c998c75515 move examples/state.rs to examples/state 2017-12-30 21:08:54 +01:00
f1f5b23e77 fix readme examples/signal 2017-12-30 21:08:12 +01:00
76b03851e6 fix examples - disable signal if windows 2017-12-30 21:05:03 +01:00
87188e1505 minor fix examples/websocket-chat 2017-12-30 16:50:49 +01:00
df393df547 move example/basic.rs to examples/basic 2017-12-30 16:50:17 +01:00
a1dc5a6bd1 update examples/tls/README 2017-12-30 16:24:50 +01:00
8e580ef7b9 add README examples/template_tera 2017-12-30 16:10:29 +01:00
12345004dd add README examples/signals 2017-12-30 16:10:00 +01:00
a1a77600c6 add README example/multipart 2017-12-30 16:09:39 +01:00
d7d9e8c0e9 update json example 2017-12-30 16:08:18 +01:00
e93af57fa7 add json example link 2017-12-30 16:07:39 +01:00
a166fc82f4 add json-rust example 2017-12-30 15:24:12 +01:00
2d769f805a add diesel+postgresql link 2017-12-30 12:21:34 +01:00
6ea894547d better application handling, fix url_for method for routes with prefix 2017-12-29 14:04:13 -08:00
491d43aa8c update tests 2017-12-29 11:49:36 -08:00
1baead993a call poll_io recursevely aftre drain completion 2017-12-29 11:45:56 -08:00
3d3e4dae9a refactor IntoHttpHandler trait 2017-12-29 11:33:04 -08:00
1d195a2cf2 make Pipeline private 2017-12-29 09:16:50 -08:00
d87fafb563 fix and refactor middleware runner 2017-12-29 01:01:31 -08:00
308df19865 update readme 2017-12-28 16:27:08 -08:00
538fea8027 add graceful shutdown system 2017-12-28 16:25:47 -08:00
3f4898a6d1 add StopWorker message 2017-12-28 13:07:29 -08:00
6a2bb9a473 split worker code to separate module 2017-12-28 12:38:37 -08:00
d8b0ce88a5 fix guide example 2017-12-28 12:27:46 -08:00
783e19c1bf fix RequestSession impl for HttpRequest 2017-12-28 11:43:45 -08:00
d80a0c9f94 add support for unix signals 2017-12-28 11:36:20 -08:00
02b37570f4 flaky test 2017-12-28 09:11:25 -08:00
820404cdd2 Merge pull request #20 from ami44/master
set session name
2017-12-28 04:23:41 -08:00
27b0dfd761 set sessio name 2017-12-28 13:02:46 +01:00
b714e1f4ce fix example 2017-12-27 19:29:04 -08:00
093d0bae40 Param ctor is private 2017-12-27 19:19:28 -08:00
8941557da6 add parameter container iterator 2017-12-27 19:09:36 -08:00
6bb893deab use Params object for query 2017-12-27 19:02:29 -08:00
19e1c1b75b use Cow for Params type 2017-12-27 18:41:09 -08:00
556de72932 add server spawn method 2017-12-27 17:49:10 -08:00
4d741b4de5 Fix typos 2017-12-27 13:26:31 -08:00
0589f2ee49 add server management commands 2017-12-27 12:58:32 -08:00
da8aa8b988 use mio for accept loop 2017-12-27 11:22:27 -08:00
be1cd2936d check example in 1.20 2017-12-27 09:49:59 -08:00
e1fb32c6e5 Update .travis.yml 2017-12-27 07:36:24 -08:00
5df5cc7374 fix guide example 2017-12-26 21:33:23 -08:00
0d21c2da22 various typos 2017-12-26 21:07:51 -08:00
183bcd38f8 modify unused_addr method; update websockt guide section 2017-12-26 20:52:21 -08:00
3abd0db6b1 restore server start test 2017-12-26 20:07:31 -08:00
29adc20581 rename module 2017-12-26 19:59:41 -08:00
743235b8fd add unit test helper 2017-12-26 19:48:02 -08:00
7f77ba557d add testing section to guide 2017-12-26 17:14:37 -08:00
d3b7d2d6b3 allow to use application factory for test server 2017-12-26 16:47:55 -08:00
f6510161b5 add simple TestServer for integrational tests cases 2017-12-26 16:35:00 -08:00
e3b0f02794 fix type for disable feartures 2017-12-26 15:17:20 -08:00
9521de5746 HttpServer::addrs() return all bound socket addresses 2017-12-26 14:45:38 -08:00
dd3a2aa68a add HttpServer::server_hostname method 2017-12-26 14:36:03 -08:00
cce9c68a10 add doc string 2017-12-26 12:46:27 -08:00
5e17a846af add notes on sync primitives 2017-12-26 11:19:08 -08:00
030a70841a add link to gitter 2017-12-26 11:04:25 -08:00
e4bfef9d26 fix tests 2017-12-26 09:28:24 -08:00
cf8c2ca95e refactor Handler trait, use mut self 2017-12-26 09:00:45 -08:00
b61a07a320 more info for middleware guide 2017-12-26 07:58:21 -08:00
ffb5742b71 fix tests 2017-12-25 19:42:55 -08:00
465a87a7cf right version 2017-12-25 13:44:50 -08:00
5b65987f6a write response optimizations 2017-12-25 13:40:06 -08:00
89c9dfb5bc update getting started guide section 2017-12-25 08:19:33 -08:00
a578262f73 update json example and guide info 2017-12-25 08:12:13 -08:00
b0c8fa03f0 use specific nightly version for appveyor 2017-12-25 07:33:05 -08:00
98b0e023f3 optimize payload detection 2017-12-25 07:31:12 -08:00
012d55e424 Set nightly version 2017-12-25 05:49:56 -08:00
f1e82ebc1e better connect handling 2017-12-24 16:15:40 -08:00
ddd9c24bb2 optimize payload type detection 2017-12-24 14:29:19 -08:00
9f9c75d832 simplify drain feature 2017-12-24 11:58:09 -08:00
eaab28cd3b proper fix for compression 2017-12-21 12:57:59 -08:00
c35d294611 fix compression 2017-12-21 12:54:18 -08:00
18f3841783 update test 2017-12-20 23:36:52 -08:00
0567e6fb0a fix typos in guide 2017-12-20 23:27:30 -08:00
55534bff8c simplify guide examples 2017-12-20 23:21:26 -08:00
bca1dd4f9e update doc strings 2017-12-20 23:19:21 -08:00
0a68811dce cleanup more examples 2017-12-20 21:06:04 -08:00
406d2c41e9 add doc string 2017-12-20 20:56:17 -08:00
63ddc07ccb added JsonBody future 2017-12-20 20:30:54 -08:00
33b2be3281 move json responder to separate module 2017-12-20 17:51:28 -08:00
bf23aa5d4b move db code to separate module 2017-12-20 17:44:19 -08:00
3c5fd18e02 cleanup examples 2017-12-20 16:32:31 -08:00
4dd3382ac7 update example 2017-12-20 16:13:21 -08:00
50891986bc simplify json example 2017-12-20 16:05:07 -08:00
df2aa42dad cleanup example 2017-12-20 15:45:26 -08:00
c36ad06332 more general Responder implementaiton for response future 2017-12-20 15:26:28 -08:00
821c96c37c check json example during travis build 2017-12-20 15:20:28 -08:00
cbb81bc747 json request example 2017-12-20 15:12:43 -08:00
79f047f5be remove box from predicates 2017-12-20 13:23:50 -08:00
813b56ebe5 make async handler future more generic 2017-12-20 12:51:39 -08:00
c3a39e026d Merge branch 'master' of github.com:actix/actix-web 2017-12-20 11:37:36 -08:00
e05596b65d upgrade actix min version 2017-12-20 11:37:27 -08:00
65767558fb Update README.md 2017-12-20 08:00:28 -08:00
7fc7d6e17a update guide 2017-12-19 22:36:06 -08:00
c47e2ccfee update guide examples 2017-12-19 18:44:17 -08:00
d0c01c2cdd update guide example 2017-12-19 18:38:02 -08:00
50b2f62c80 update guide section about ssl 2017-12-19 18:36:29 -08:00
0a96b8c579 update readme 2017-12-19 16:17:27 -08:00
d41aade0b7 update readme 2017-12-19 16:14:47 -08:00
626999bcc9 update doc strings 2017-12-19 16:09:19 -08:00
64d867d9a1 update session guide section 2017-12-19 15:44:25 -08:00
1596f4db73 refactor url encoded body parsing 2017-12-19 14:03:01 -08:00
fa2a3bc55e make method private 2017-12-19 13:11:19 -08:00
52c9865716 split examples check 2017-12-19 12:22:11 -08:00
db7bd962cb fix some doc strings 2017-12-19 11:46:11 -08:00
f858fa7a32 Merge branch 'travis-test' 2017-12-19 11:35:16 -08:00
2bad99b645 better query() method impl; update doc strings 2017-12-19 11:34:51 -08:00
566066e855 check examples 2017-12-19 10:56:48 -08:00
009874125e add client.py comments 2017-12-19 10:25:23 -08:00
2e790dfcc6 add multipart guide section 2017-12-19 10:10:03 -08:00
e3f9345420 multipart field is stream of bytes 2017-12-19 09:55:49 -08:00
790793f8a1 refactor multipart stream creation 2017-12-19 09:51:28 -08:00
13cbfc877d simplify server start method 2017-12-19 09:08:36 -08:00
4f6145e5c7 fix typos 2017-12-19 00:29:25 -08:00
f3b853f224 refactor payload 2017-12-19 00:18:57 -08:00
0cab873066 make payload sender public 2017-12-18 21:58:38 -08:00
64dc6c5771 fix typos 2017-12-18 20:03:42 -08:00
669975df75 fix typos 2017-12-18 20:00:57 -08:00
56fd088163 added database integration guide section 2017-12-18 19:38:16 -08:00
2124730e0a guide update 2017-12-18 18:56:58 -08:00
e9a3845e26 update license in readme 2017-12-18 16:48:30 -08:00
3f0e7456c0 update examples links 2017-12-18 16:42:58 -08:00
1e1da5832f better name 2017-12-18 16:40:33 -08:00
625c4ad0db add more comments 2017-12-18 16:30:35 -08:00
fde94bfe95 added diesel example 2017-12-18 16:25:26 -08:00
3e8a6c3988 add tera example 2017-12-18 13:41:52 -08:00
26af6040ff update tests 2017-12-18 13:26:43 -08:00
9ed4159c0c update examples 2017-12-18 13:06:41 -08:00
27d92f3a23 refactor server bind and start process 2017-12-17 12:35:04 -08:00
4b421b44a2 add mit license 2017-12-17 10:08:44 -08:00
1a51f75ecc update readme 2017-12-17 10:03:37 -08:00
9821c6ea90 update readme 2017-12-16 11:39:56 -08:00
167717d20e update readme 2017-12-16 11:22:39 -08:00
91ffab8f6e update readme 2017-12-16 07:30:53 -08:00
b1f33e29ec simplify content-length calculation 2017-12-16 07:29:15 -08:00
ed8bd3d6a3 h1 cleanups 2017-12-15 22:49:48 -08:00
1daf50095a cleanup response 2017-12-15 20:00:12 -08:00
a8b2f1b821 update tests 2017-12-15 18:49:11 -08:00
1ddcce7b76 hide httpresponse box 2017-12-15 16:24:15 -08:00
c3d5e4301a cleanup h1 parse 2017-12-15 13:10:12 -08:00
71c37bde5a Update README.md 2017-12-15 05:44:10 -08:00
4913e7d3c2 cleanup 2017-12-14 22:22:27 -08:00
d77156c16c fix readme 2017-12-14 20:49:09 -08:00
106f43e874 better SharedBytes usage for h2 2017-12-14 20:48:31 -08:00
2b0994e448 update tests 2017-12-14 20:29:49 -08:00
a2dff8a0b9 update readme 2017-12-14 20:12:28 -08:00
c37565cc4a various server optimizations 2017-12-14 19:34:31 -08:00
b61c2a0cf0 handle keep-alive setting more efficient 2017-12-14 11:20:45 -08:00
c98d320f8c rename FromRequest trait to Responder 2017-12-14 09:43:42 -08:00
355f54efe2 update api docs 2017-12-13 23:35:21 -08:00
8c1487f7f2 update tests 2017-12-13 23:09:35 -08:00
4529efa948 rename module 2017-12-13 22:54:52 -08:00
9d0a64ac98 remove unused file 2017-12-13 22:43:16 -08:00
b7cde3f4a9 update guide 2017-12-13 22:36:28 -08:00
408ddf0be1 add ssl guide ref 2017-12-13 21:56:30 -08:00
406ef20262 add readme 2017-12-13 21:44:16 -08:00
c2751efa87 refactor keep-alive; update guide 2017-12-13 21:38:47 -08:00
653b431895 fix example 2017-12-13 17:28:16 -08:00
96f598f2c4 various optimizations 2017-12-13 16:44:35 -08:00
81f8da03ae refactor http workers 2017-12-13 12:47:07 -08:00
6b61041aec move tests 2017-12-13 11:16:26 -08:00
d4187f682b various cleanups 2017-12-13 11:10:03 -08:00
55204c829c update tests 2017-12-13 08:00:25 -08:00
2e83c5924d cleanup and optimize some code 2017-12-12 21:32:58 -08:00
ab6efd2421 handle http connections in different threads 2017-12-12 17:21:00 -08:00
55818028cb state does not need to be Send 2017-12-12 08:51:16 -08:00
e9aa67b75d http server accepts factory of HttpHandlers 2017-12-12 07:40:36 -08:00
b9da09ddf0 update readme 2017-12-11 19:17:37 -08:00
d7efbb516d fix guide tests 2017-12-11 19:16:45 -08:00
6e3f598c50 fix guide page 2017-12-11 16:50:51 -08:00
007b7ce62f unify route not found handling 2017-12-11 16:26:51 -08:00
b1ae7f95cc update readme example 2017-12-11 15:57:37 -08:00
96381f5d6a fix doc 2017-12-11 14:27:09 -08:00
0f75d066f2 simplify Application creation; update url dispatch guide section 2017-12-11 14:16:29 -08:00
caca907c23 update guide 2017-12-09 14:06:22 -08:00
c5490a851c add guid for path normalization 2017-12-09 13:58:24 -08:00
0388a464ba tests for NormalizePath 2017-12-09 13:25:06 -08:00
71bbe2a5dd update doc string for NormalizePath 2017-12-09 11:55:55 -08:00
7addd2800d add NormalizePath handler 2017-12-09 11:39:13 -08:00
273de2260d refactor pipeline 2017-12-09 05:54:04 -08:00
b98ab2eebe use trait instead of pipeline 2017-12-09 04:33:40 -08:00
4a40b026a4 more error wrappers 2017-12-08 15:52:46 -08:00
a44f71d8c2 make ErrorBadRequest type useful 2017-12-08 15:25:37 -08:00
9043e7286d tests for default predicates 2017-12-08 12:51:44 -08:00
3e91b06241 fix static files 2017-12-08 12:29:28 -08:00
774bfc0a86 use server settings for scheme and host values 2017-12-08 09:48:53 -08:00
1293619096 set server settings to HttpHandler 2017-12-08 09:24:05 -08:00
2192d14eff added ServerSettings 2017-12-07 22:54:44 -08:00
b71ddf7b4c pass local addr to channel; use bitflags 2017-12-07 21:52:46 -08:00
3f06439d3e update examples 2017-12-07 18:08:16 -08:00
d595dd850e load cookies automatically 2017-12-07 18:00:20 -08:00
0abb3863dc simplify api 2017-12-07 17:38:18 -08:00
dff7618f35 rearrange exports 2017-12-07 16:40:29 -08:00
968f5d39d6 added external resources; refactor route recognizer 2017-12-07 16:22:26 -08:00
9e3aa59155 ignore tests 2017-12-07 10:23:14 -08:00
2a0d5db41a more tests 2017-12-06 18:39:13 -08:00
4b03d03404 rearrange exports 2017-12-06 17:06:40 -08:00
9ea0781aba fix test 2017-12-06 16:58:49 -08:00
63502fa833 test for Router::has_route 2017-12-06 16:40:23 -08:00
a18bd5dac0 add doc ref 2017-12-06 16:34:54 -08:00
0dd27bd224 added HttpRequest::url_for 2017-12-06 16:26:27 -08:00
8d52e2bbd9 tests for default resource 2017-12-06 13:02:53 -08:00
c63f058647 simplify application creation 2017-12-06 11:00:39 -08:00
87c7441f7d remove Applicaiton::route, resource is enough 2017-12-06 08:03:08 -08:00
04ded5ba68 hide pkcs 2017-12-06 07:49:01 -08:00
903b391e0a move ConnectionInfo to dev 2017-12-06 07:47:42 -08:00
c2bfc091bd fix travis 2017-12-05 22:14:38 -08:00
20af8822fd cleanup 2017-12-05 21:53:00 -08:00
d7e65b6212 add ConnectionInfo tests 2017-12-05 21:41:30 -08:00
c3de32c3b3 added ConnectionInfo 2017-12-05 17:09:15 -08:00
d8b880e167 work on resource_path api 2017-12-05 13:31:06 -08:00
3de43c2a46 update readme 2017-12-05 12:25:57 -08:00
bd1e9abdd8 simple readme example 2017-12-05 11:50:09 -08:00
86d7290f9e update tests 2017-12-05 11:43:41 -08:00
a83d9b24ae extrat elements of path pattern 2017-12-05 11:31:35 -08:00
3c9b6ea619 update guide 2017-12-04 20:38:38 -08:00
fd6b243cd6 update examples 2017-12-04 16:32:31 -08:00
2950c90c77 doc fixes 2017-12-04 16:26:40 -08:00
f4e9fc7b6a rename async to a 2017-12-04 16:09:22 -08:00
e98972e93b exclude example from code coverage 2017-12-04 15:35:07 -08:00
e332c1242f use Route for Applicaiton handlers 2017-12-04 14:53:40 -08:00
f5d6179a34 renamed Route::handler to Route::f, added Route::h to register Handler 2017-12-04 14:07:53 -08:00
03f7d95d88 fix fmratting 2017-12-04 13:36:28 -08:00
a163e75318 drop tail path pattern 2017-12-04 13:34:55 -08:00
3bf3738e65 introduce route predicates 2017-12-04 13:32:05 -08:00
57fd35ffc1 added default headers middleware 2017-12-03 20:47:15 -08:00
d35be02587 cleanup 2017-12-03 20:09:46 -08:00
57c53bd2a0 add encoding section 2017-12-03 18:58:15 -08:00
319e9bbd05 added Json response support 2017-12-03 18:51:52 -08:00
5decff9154 added fs tests 2017-12-03 18:15:09 -08:00
69f0c098e3 check show_index 2017-12-03 16:58:31 -08:00
5abc46034a refactor static files 2017-12-03 16:57:25 -08:00
7c6faaa8e0 add Item and Error to FromRequest trait 2017-12-03 14:22:04 -08:00
6bc7d60f52 more default impls for FromRequest 2017-12-02 17:14:55 -08:00
fb3185de94 rename module 2017-12-02 16:47:02 -08:00
61744b68a1 introduce custom FromRequest traint for conversion into Reply 2017-12-02 16:37:21 -08:00
187948ddd1 error response for io::Error 2017-12-02 14:58:22 -08:00
29a26b3236 code cleanup 2017-12-02 12:14:16 -08:00
d0b9d9c1d6 content-encoding; try cargo tarpaulin 2017-12-02 11:41:20 -08:00
0fc01c48d1 return bad request for param parse error 2017-12-02 11:03:41 -08:00
ebfd3ac275 tests for PathBuf::from_param 2017-12-02 10:43:14 -08:00
d8f27e95a6 added FromParam trait for path segment conversions, FramParam impl for PathBuf 2017-12-02 10:18:54 -08:00
d03d1207a8 add http2 section 2017-12-02 00:36:50 -08:00
8f33dec026 add logging doc section 2017-12-02 00:24:26 -08:00
0dae109172 missing files 2017-12-01 23:42:21 -08:00
1a5df7192e add multiple apps example 2017-12-01 23:32:15 -08:00
3ffd36eee2 some more guide 2017-12-01 23:06:15 -08:00
c3a0a4457a add appl builder async method; add async handler section 2017-12-01 21:58:19 -08:00
e6feec62a8 simplier examples 2017-12-01 21:31:38 -08:00
f0c346f18c handler info 2017-12-01 21:29:22 -08:00
186726fbad tests for Completed state 2017-12-01 19:57:34 -08:00
97bed17fd2 test for completed pipeline state 2017-12-01 16:10:01 -08:00
47645626c4 refactor pipeline 2017-12-01 15:45:15 -08:00
9a1ba527c0 recognizer tests 2017-11-30 19:34:33 -08:00
3fcd5f6935 use http::Uri for uri parsing 2017-11-30 19:01:25 -08:00
7135c0163b simlify code 2017-11-30 18:27:27 -08:00
f53f35f364 added tail pattern 2017-11-30 15:48:09 -08:00
07cc017320 make Task private 2017-11-30 15:13:56 -08:00
271a292ea5 no need to store disconnected state on task 2017-11-30 14:44:58 -08:00
6e138bf373 refactor streaming responses 2017-11-30 14:42:20 -08:00
a0bca2d4cf fix typo 2017-11-30 07:42:02 -08:00
6c4fdf604b do not set content encoding header for upgraded connection 2017-11-29 19:40:27 -08:00
e4f8551cba do not com press upgrade connection 2017-11-29 19:36:55 -08:00
559b1c50a3 do not encode payload less that 1024 bytes 2017-11-29 19:18:37 -08:00
d2eae3d5b3 simplify Handler trait 2017-11-29 15:10:45 -08:00
27035c9454 fixes 2017-11-29 14:22:47 -08:00
991dd107b1 update ws doc 2017-11-29 14:12:27 -08:00
ffb2e3c0ab update examples 2017-11-29 14:03:18 -08:00
acc2fff655 export and simplify HttpHandler trait 2017-11-29 13:53:52 -08:00
427566b90d export Handler 2017-11-29 13:41:51 -08:00
16ceb741b8 refactor RouteHandler trait 2017-11-29 13:26:55 -08:00
6f833798c7 refactor http actor handling 2017-11-29 10:31:24 -08:00
6177d86d97 refactor handler rtype handling 2017-11-29 09:17:00 -08:00
e9bfab8012 add deref for payload item 2017-11-28 19:51:39 -08:00
afeecea05f refactor reply handling 2017-11-28 19:49:17 -08:00
6f5b58b691 more guide 2017-11-28 18:00:10 -08:00
987b275c3f add response test 2017-11-28 14:29:22 -08:00
932e751240 add status code helper method for http response 2017-11-28 14:23:42 -08:00
706e2a07de add helper converters into response 2017-11-28 13:52:53 -08:00
a3022e6d88 some overview text for guide 2017-11-28 12:44:59 -08:00
b55d69b4c2 better handler result handling 2017-11-28 12:42:53 -08:00
0bd8725426 make resource handler result more generic 2017-11-28 12:28:51 -08:00
ac3fe30d19 use git master for examples 2017-11-28 10:27:58 -08:00
88eb6bc6a2 build mdbook 2017-11-28 09:57:27 -08:00
91cefa8e2f use precompiled mdbook 2017-11-28 09:40:22 -08:00
26413d1d61 add link to guide 2017-11-27 19:56:14 -08:00
06f9b7b52f fix travis config 2017-11-27 16:49:25 -08:00
599f3c26e0 start working on guide 2017-11-27 16:41:37 -08:00
b5a4f6f855 hellper method for json body 2017-11-27 10:39:47 -08:00
42716d3252 update tests 2017-11-26 22:59:04 -08:00
170d3163f3 better export naming 2017-11-26 22:53:28 -08:00
0519056199 consistent naming 2017-11-26 22:31:29 -08:00
45433f71e5 impl Default trait for HttpRequest 2017-11-26 22:20:28 -08:00
b62b303fdb remove unneeded directives 2017-11-26 22:11:51 -08:00
8e80fed2af added urlencoded errors 2017-11-26 22:00:25 -08:00
fdafb0c848 simplify middleware api; fix examples 2017-11-26 21:47:33 -08:00
5a3b6638a7 move state to request object 2017-11-26 21:18:38 -08:00
8e0a7f44d4 pass request by value 2017-11-26 20:34:20 -08:00
eb7f48a1c6 include payload into request 2017-11-26 19:00:57 -08:00
32483735ba cookie session implementation 2017-11-26 17:34:11 -08:00
53ce186294 cleanup pipeline 2017-11-25 12:05:27 -08:00
37c1e78c7a added helper Task::error method 2017-11-25 10:52:43 -08:00
45ecb87eab allow middlware error result 2017-11-25 10:24:45 -08:00
54bbc98343 cookie session prototype 2017-11-25 09:52:32 -08:00
f4972150cc better middleware error handling 2017-11-25 09:40:57 -08:00
1fc64bc83d better pipeline error handling 2017-11-25 09:28:25 -08:00
64ade803f9 store error for error response 2017-11-25 09:03:44 -08:00
940bc08aba remove unused imports 2017-11-24 22:19:06 -08:00
7569036dd4 refactor request pipeline 2017-11-24 22:15:52 -08:00
59b8214685 better nightly detection 2017-11-24 10:28:43 -08:00
f33c489154 added default ErrorResponse for std::error::Error 2017-11-24 10:03:13 -08:00
5529ea0428 better logger format test 2017-11-23 16:53:02 -08:00
39a20fb95d use env_logger for logger tests 2017-11-23 15:48:59 -08:00
155a636487 more logger tests 2017-11-23 15:37:11 -08:00
e571587a8c refactor logger middleware 2017-11-23 15:17:16 -08:00
5945035fc3 better method name 2017-11-19 18:55:37 -10:00
766e243c63 more Body tests 2017-11-19 18:32:37 -10:00
83862dfbb4 update payload tests 2017-11-19 18:26:30 -10:00
c44e4ad100 expect error tests 2017-11-19 18:02:31 -10:00
1a0e87ac3c add tests for errors 2017-11-19 17:58:47 -10:00
72edd75eab add custom ExceptError 2017-11-19 17:51:14 -10:00
78d8d21196 cleanup error 2017-11-19 17:26:31 -10:00
fd3dcdf0f6 use failure from crates 2017-11-18 06:50:56 -10:00
a87784ba15 use Result intead of HandlerResult 2017-11-18 06:50:07 -10:00
c800bf55f5 update tests 2017-11-15 20:28:02 -10:00
0143e18fe9 fix extern crate 2017-11-15 20:09:37 -10:00
de71ad7de4 refactor error handling 2017-11-15 20:06:28 -10:00
c565965865 rename BinaryBody 2017-11-10 13:42:32 -08:00
f2520d2d79 update logger doc 2017-11-10 13:34:16 -08:00
be3a1ab770 use remote addr in logger if available 2017-11-10 13:26:12 -08:00
f369d9af0e make remote addr available to http request 2017-11-10 13:08:15 -08:00
265628750c refactor logger middleware 2017-11-10 12:29:54 -08:00
657efb8cce fix readme 2017-11-09 22:18:59 -08:00
40c1d3b711 refactor middlewares 2017-11-09 22:08:54 -08:00
51cd08ef57 store cookies load state 2017-11-08 21:01:56 -08:00
2a319d733f enable secure cookies 2017-11-08 20:57:59 -08:00
519a9e64f8 cleanup tls example 2017-11-08 20:29:48 -08:00
4d575c6269 update readme 2017-11-08 20:25:14 -08:00
e9fe2ba740 use bytes::Writer 2017-11-08 20:08:16 -08:00
7565ed8e06 use higher pripority for br 2017-11-08 19:42:13 -08:00
02fb424659 add custom Debug impl for HttpResponse 2017-11-08 19:31:25 -08:00
1392b2b171 Update README.md 2017-11-08 16:48:20 -08:00
e558414867 add response content encoding 2017-11-08 16:44:46 -08:00
76b8104f52 use version in readme 2017-11-07 16:09:37 -08:00
2eb3ad0de3 better name 2017-11-07 16:08:10 -08:00
6974213036 use new brotli2 version 2017-11-07 15:59:37 -08:00
72c8ad9fe1 fix appveyor config for gnu target 2017-11-07 09:43:39 -08:00
a65fd695e1 refactor content encoding 2017-11-06 16:23:58 -08:00
994d0afd80 allow to set/change responses content encoding 2017-11-06 14:56:38 -08:00
2379bcbf39 added content-encoding support to h2 2017-11-06 09:35:52 -08:00
bddd8e9c2e better deflate decoding 2017-11-06 09:24:19 -08:00
c2978a6eea add content encoding decompression 2017-11-06 01:27:46 -08:00
b467ddf970 test with alpn feature 2017-11-04 15:16:49 -07:00
3f649b8e07 fix name 2017-11-04 14:07:15 -07:00
e9e247217a update readme 2017-11-04 13:49:51 -07:00
f23974cfb5 update readme 2017-11-04 13:49:05 -07:00
53868a88fa add keep-alive for h2 connection 2017-11-04 13:24:57 -07:00
28652a3ba8 update readme 2017-11-04 12:36:37 -07:00
41be1db8bc fix link 2017-11-04 12:35:55 -07:00
67f3ad31ab update readme 2017-11-04 12:35:19 -07:00
d7d3d663e9 refactor server impl and add support for alpn http2 negotiation 2017-11-04 12:33:14 -07:00
32cefb8455 implement h2 writer 2017-11-04 09:07:44 -07:00
4add742aba refactor task impl, extract stream writer to separate struct 2017-11-03 13:48:00 -07:00
f010672885 rename modules 2017-11-03 13:48:00 -07:00
c14e6c9008 make possible to use async handler 2017-11-03 13:35:34 -07:00
ec3b139273 Allow to start tls server with HttpServer::serve_tls 2017-11-01 16:34:58 -07:00
a12e5e9cf5 bump version 2017-10-30 20:46:39 -07:00
147 changed files with 38523 additions and 8372 deletions

View File

@ -3,6 +3,15 @@ environment:
PROJECT_NAME: actix
matrix:
# Stable channel
- TARGET: i686-pc-windows-gnu
CHANNEL: 1.24.0
- TARGET: i686-pc-windows-msvc
CHANNEL: 1.24.0
- TARGET: x86_64-pc-windows-gnu
CHANNEL: 1.24.0
- TARGET: x86_64-pc-windows-msvc
CHANNEL: 1.24.0
# Stable channel
- TARGET: i686-pc-windows-gnu
CHANNEL: stable
- TARGET: i686-pc-windows-msvc
@ -33,6 +42,12 @@ environment:
# Install Rust and Cargo
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
install:
- ps: >-
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
$Env:PATH += ';C:\msys64\mingw64\bin'
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
$Env:PATH += ';C:\MinGW\bin'
}
- curl -sSf -o rustup-init.exe https://win.rustup.rs
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
@ -44,6 +59,4 @@ build: false
# Equivalent to Travis' `script` phase
test_script:
- cargo build --no-default-features
- cargo clean
- cargo test --no-default-features
- cargo test --no-default-features --features="flate2-rust"

5
.gitignore vendored
View File

@ -1,7 +1,7 @@
target/
Cargo.lock
target/
guide/build/
/gh-pages
__pycache__
*.so
*.out
@ -9,7 +9,6 @@ __pycache__
*.pid
*.sock
*~
*.egg-info/
# These are backup files generated by rustfmt
**/*.rs.bk

View File

@ -1,69 +1,55 @@
language: rust
rust:
- 1.20.0
- stable
- beta
- nightly
sudo: required
sudo: false
dist: trusty
cache:
cargo: true
apt: true
matrix:
include:
- rust: 1.24.0
- rust: stable
- rust: beta
- rust: nightly
allow_failures:
- rust: nightly
env:
global:
- RUSTFLAGS="-C link-dead-code"
# - RUSTFLAGS="-C link-dead-code"
- OPENSSL_VERSION=openssl-1.0.2
addons:
apt:
packages:
- libcurl4-openssl-dev
- libelf-dev
- libdw-dev
- cmake
- gcc
- binutils-dev
- libiberty-dev
before_install:
- sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl
- sudo apt-get update -qq
- sudo apt-get install -qq libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev
# Add clippy
before_script:
- |
if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
( ( cargo install clippy && export CLIPPY=true ) || export CLIPPY=false );
fi
- export PATH=$PATH:~/.cargo/bin
script:
- cargo test --no-default-features
- |
if [[ "$TRAVIS_RUST_VERSION" == "nightly" && $CLIPPY ]]; then
cargo clippy
if [[ "$TRAVIS_RUST_VERSION" != "1.24.0" ]]; then
cargo clean
cargo test --features="alpn,tls" -- --nocapture
fi
- |
if [[ "$TRAVIS_RUST_VERSION" == "1.24.0" ]]; then
bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh)
USE_SKEPTIC=1 cargo tarpaulin --out Xml --no-count
bash <(curl -s https://codecov.io/bash)
echo "Uploaded code coverage"
fi
# Upload docs
after_success:
- |
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
cargo doc --no-deps &&
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then
cargo doc --features "alpn, tls, session" --no-deps &&
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 &&
./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"
fi
- |
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz &&
tar xzf master.tar.gz &&
cd kcov-master &&
mkdir build &&
cd build &&
cmake .. &&
make &&
make install DESTDIR=../../kcov-build &&
cd ../.. &&
rm -rf kcov-master &&
for file in target/debug/actix_web-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done &&
for file in target/debug/test_*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done &&
bash <(curl -s https://codecov.io/bash) &&
echo "Uploaded code coverage"
fi

View File

@ -1,4 +1,435 @@
# CHANGES
# Changes
## [0.6.14] - 2018-06-21
### Added
* Allow to disable masking for websockets client
### Fixed
* SendRequest execution fails with the "internal error: entered unreachable code" #329
## [0.6.13] - 2018-06-13
### Fixed
* http/2 end-of-frame is not set if body is empty bytes #307
* InternalError can trigger memory unsafety #301
* Fix docs.rs build
## [0.6.12] - 2018-06-08
### Added
* Add `Host` filter #287
* Allow to filter applications
* Improved failure interoperability with downcasting #285
* Allow to use custom resolver for `ClientConnector`
### Deprecated
* `Error::cause()` and introduces failure interoperability functions and downcasting.
## [0.6.11] - 2018-06-05
### Fixed
* Support chunked encoding for UrlEncoded body #262
* `HttpRequest::url_for()` for a named route with no variables segments #265
* `Middleware::response()` is not invoked if error result was returned by another `Middleware::start()` #255
* CORS: Do not validate Origin header on non-OPTION requests #271
* Fix multipart upload "Incomplete" error #282
## [0.6.10] - 2018-05-24
### Added
* Allow to use path without traling slashes for scope registration #241
* Allow to set encoding for exact NamedFile #239
### Fixed
* `TestServer::post()` actually sends `GET` request #240
## 0.6.9 (2018-05-22)
* Drop connection if request's payload is not fully consumed #236
* Fix streaming response with body compression
## 0.6.8 (2018-05-20)
* Fix scope resource path extractor #234
* Re-use tcp listener on pause/resume
## 0.6.7 (2018-05-17)
* Fix compilation with --no-default-features
## 0.6.6 (2018-05-17)
* Panic during middleware execution #226
* Add support for listen_tls/listen_ssl #224
* Implement extractor for `Session`
* Ranges header support for NamedFile #60
## 0.6.5 (2018-05-15)
* Fix error handling during request decoding #222
## 0.6.4 (2018-05-11)
* Fix segfault in ServerSettings::get_response_builder()
## 0.6.3 (2018-05-10)
* Add `Router::with_async()` method for async handler registration.
* Added error response functions for 501,502,503,504
* Fix client request timeout handling
## 0.6.2 (2018-05-09)
* WsWriter trait is optional.
## 0.6.1 (2018-05-08)
* Fix http/2 payload streaming #215
* Fix connector's default `keep-alive` and `lifetime` settings #212
* Send `ErrorNotFound` instead of `ErrorBadRequest` when path extractor fails #214
* Allow to exclude certain endpoints from logging #211
## 0.6.0 (2018-05-08)
* Add route scopes #202
* Allow to use ssl and non-ssl connections at the same time #206
* Websocket CloseCode Empty/Status is ambiguous #193
* Add Content-Disposition to NamedFile #204
* Allow to access Error's backtrace object
* Allow to override files listing renderer for `StaticFiles` #203
* Various extractor usability improvements #207
## 0.5.6 (2018-04-24)
* Make flate2 crate optional #200
## 0.5.5 (2018-04-24)
* Fix panic when Websocket is closed with no error code #191
* Allow to use rust backend for flate2 crate #199
## 0.5.4 (2018-04-19)
* Add identity service middleware
* Middleware response() is not invoked if there was an error in async handler #187
* Use Display formatting for InternalError Display implementation #188
## 0.5.3 (2018-04-18)
* Impossible to quote slashes in path parameters #182
## 0.5.2 (2018-04-16)
* Allow to configure StaticFiles's CpuPool, via static method or env variable
* Add support for custom handling of Json extractor errors #181
* Fix StaticFiles does not support percent encoded paths #177
* Fix Client Request with custom Body Stream halting on certain size requests #176
## 0.5.1 (2018-04-12)
* Client connector provides stats, `ClientConnector::stats()`
* Fix end-of-stream handling in parse_payload #173
* Fix StaticFiles generate a lot of threads #174
## 0.5.0 (2018-04-10)
* Type-safe path/query/form parameter handling, using serde #70
* HttpResponse builder's methods `.body()`, `.finish()`, `.json()`
return `HttpResponse` instead of `Result`
* Use more ergonomic `actix_web::Error` instead of `http::Error` for `ClientRequestBuilder::body()`
* Added `signed` and `private` `CookieSessionBackend`s
* Added `HttpRequest::resource()`, returns current matched resource
* Added `ErrorHandlers` middleware
* Fix router cannot parse Non-ASCII characters in URL #137
* Fix client connection pooling
* Fix long client urls #129
* Fix panic on invalid URL characters #130
* Fix logger request duration calculation #152
* Fix prefix and static file serving #168
## 0.4.10 (2018-03-20)
* Use `Error` instead of `InternalError` for `error::ErrorXXXX` methods
* Allow to set client request timeout
* Allow to set client websocket handshake timeout
* Refactor `TestServer` configuration
* Fix server websockets big payloads support
* Fix http/2 date header generation
## 0.4.9 (2018-03-16)
* Allow to disable http/2 support
* Wake payload reading task when data is available
* Fix server keep-alive handling
* Send Query Parameters in client requests #120
* Move brotli encoding to a feature
* Add option of default handler for `StaticFiles` handler #57
* Add basic client connection pooling
## 0.4.8 (2018-03-12)
* Allow to set read buffer capacity for server request
* Handle WouldBlock error for socket accept call
## 0.4.7 (2018-03-11)
* Fix panic on unknown content encoding
* Fix connection get closed too early
* Fix streaming response handling for http/2
* Better sleep on error support
## 0.4.6 (2018-03-10)
* Fix client cookie handling
* Fix json content type detection
* Fix CORS middleware #117
* Optimize websockets stream support
## 0.4.5 (2018-03-07)
* Fix compression #103 and #104
* Fix client cookie handling #111
* Non-blocking processing of a `NamedFile`
* Enable compression support for `NamedFile`
* Better support for `NamedFile` type
* Add `ResponseError` impl for `SendRequestError`. This improves ergonomics of the client.
* Add native-tls support for client
* Allow client connection timeout to be set #108
* Allow to use std::net::TcpListener for HttpServer
* Handle panics in worker threads
## 0.4.4 (2018-03-04)
* Allow to use Arc<Vec<u8>> as response/request body
* Fix handling of requests with an encoded body with a length > 8192 #93
## 0.4.3 (2018-03-03)
* Fix request body read bug
* Fix segmentation fault #79
* Set reuse address before bind #90
## 0.4.2 (2018-03-02)
* Better naming for websockets implementation
* Add `Pattern::with_prefix()`, make it more usable outside of actix
* Add csrf middleware for filter for cross-site request forgery #89
* Fix disconnect on idle connections
## 0.4.1 (2018-03-01)
* Rename `Route::p()` to `Route::filter()`
* Better naming for http codes
* Fix payload parse in situation when socket data is not ready.
* Fix Session mutable borrow lifetime #87
## 0.4.0 (2018-02-28)
* Actix 0.5 compatibility
* Fix request json/urlencoded loaders
* Simplify HttpServer type definition
* Added HttpRequest::encoding() method
* Added HttpRequest::mime_type() method
* Added HttpRequest::uri_mut(), allows to modify request uri
* Added StaticFiles::index_file()
* Added http client
* Added websocket client
* Added TestServer::ws(), test websockets client
* Added TestServer http client support
* Allow to override content encoding on application level
## 0.3.3 (2018-01-25)
* Stop processing any events after context stop
* Re-enable write back-pressure for h1 connections
* Refactor HttpServer::start_ssl() method
* Upgrade openssl to 0.10
## 0.3.2 (2018-01-21)
* Fix HEAD requests handling
* Log request processing errors
* Always enable content encoding if encoding explicitly selected
* Allow multiple Applications on a single server with different state #49
* CORS middleware: allowed_headers is defaulting to None #50
## 0.3.1 (2018-01-13)
* Fix directory entry path #47
* Do not enable chunked encoding for HTTP/1.0
* Allow explicitly disable chunked encoding
## 0.3.0 (2018-01-12)
* HTTP/2 Support
* Refactor streaming responses
* Refactor error handling
* Asynchronous middlewares
* Refactor logger middleware
* Content compression/decompression (br, gzip, deflate)
* Server multi-threading
* Gracefull shutdown support
## 0.2.1 (2017-11-03)
* Allow to start tls server with `HttpServer::serve_tls`
* Export `Frame` enum
* Add conversion impl from `HttpResponse` and `BinaryBody` to a `Frame`
## 0.2.0 (2017-10-30)

46
CODE_OF_CONDUCT.md Normal file
View File

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

View File

@ -1,15 +1,18 @@
[package]
name = "actix-web"
version = "0.2.0"
version = "0.6.14"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix web framework"
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
readme = "README.md"
keywords = ["actor", "http", "web"]
homepage = "https://github.com/actix/actix-web"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-web/"
categories = ["network-programming", "asynchronous"]
license = "Apache-2.0"
categories = ["network-programming", "asynchronous",
"web-programming::http-server",
"web-programming::http-client",
"web-programming::websocket"]
license = "MIT/Apache-2.0"
exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"]
build = "build.rs"
@ -23,48 +26,94 @@ name = "actix_web"
path = "src/lib.rs"
[features]
default = []
default = ["session", "brotli", "flate2-c"]
# http/2
# http2 = ["h2"]
# tls
tls = ["native-tls", "tokio-tls"]
# openssl
alpn = ["openssl", "tokio-openssl"]
# sessions feature, session require "ring" crate and c compiler
session = ["cookie/secure"]
# brotli encoding, requires c compiler
brotli = ["brotli2"]
# miniz-sys backend for flate2 crate
flate2-c = ["flate2/miniz-sys"]
# rust backend for flate2 crate
flate2-rust = ["flate2/rust_backend"]
[package.metadata.docs.rs]
features = ["tls", "alpn", "session", "brotli", "flate2-c"]
[dependencies]
log = "0.3"
time = "0.1"
http = "0.1"
httparse = "0.1"
http-range = "0.1"
mime = "0.3"
mime_guess = "1.8"
cookie = { version="0.10", features=["percent-encode"] }
regex = "0.2"
sha1 = "0.2"
url = "1.5"
percent-encoding = "1.0"
actix = "^0.5.8"
# tokio
base64 = "0.9"
bitflags = "1.0"
failure = "0.1.1"
h2 = "0.1"
http = "^0.1.5"
httparse = "1.2"
http-range = "0.1"
libc = "0.2"
log = "0.4"
mime = "0.3"
mime_guess = "2.0.0-alpha"
num_cpus = "1.0"
percent-encoding = "1.0"
rand = "0.4"
regex = "1.0"
serde = "1.0"
serde_json = "1.0"
serde_urlencoded = "0.5"
sha1 = "0.6"
smallvec = "0.6"
time = "0.1"
encoding = "0.2"
language-tags = "0.2"
lazy_static = "1.0"
url = { version="1.7", features=["query_encoding"] }
cookie = { version="0.10", features=["percent-encode"] }
brotli2 = { version="^0.3.2", optional = true }
flate2 = { version="1.0", optional = true, default-features = false }
# io
mio = "^0.6.13"
net2 = "0.2"
bytes = "0.4"
byteorder = "1"
futures = "0.1"
futures-cpupool = "0.1"
slab = "0.4"
tokio-io = "0.1"
tokio-core = "0.1"
# h2 = { git = 'https://github.com/carllerche/h2', optional = true }
[dependencies.actix]
version = ">=0.3.1"
#path = "../actix"
#git = "https://github.com/actix/actix.git"
default-features = false
features = []
# native-tls
native-tls = { version="0.1", optional = true }
tokio-tls = { version="0.1", optional = true }
# openssl
openssl = { version="0.10", optional = true }
tokio-openssl = { version="0.2", optional = true }
[dev-dependencies]
env_logger = "0.4"
reqwest = "0.8"
skeptic = "0.13"
env_logger = "0.5"
serde_derive = "1.0"
[build-dependencies]
skeptic = "0.13"
version_check = "0.1"
[profile.release]
lto = true
opt-level = 3
debug = true
codegen-units = 1
[workspace]
members = [
"./",
"tools/wsload/",
]

25
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.

82
MIGRATION.md Normal file
View File

@ -0,0 +1,82 @@
## Migration from 0.5 to 0.6
* `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
* `ws::Message::Close` now includes optional close reason.
`ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
* `HttpServer::threads()` renamed to `HttpServer::workers()`.
* `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
* `HttpRequest::extensions()` returns read only reference to the request's Extension
`HttpRequest::extensions_mut()` returns mutable reference.
* Instead of
`use actix_web::middleware::{
CookieSessionBackend, CookieSessionError, RequestSession,
Session, SessionBackend, SessionImpl, SessionStorage};`
use `actix_web::middleware::session`
`use actix_web::middleware::session{CookieSessionBackend, CookieSessionError,
RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};`
* `FromRequest::from_request()` accepts mutable reference to a request
* `FromRequest::Result` has to implement `Into<Reply<Self>>`
* [`Responder::respond_to()`](
https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
is generic over `S`
* Use `Query` extractor instead of HttpRequest::query()`.
```rust
fn index(q: Query<HashMap<String, String>>) -> Result<..> {
...
}
```
or
```rust
let q = Query::<HashMap<String, String>>::extract(req);
```
* Websocket operations are implemented as `WsWriter` trait.
you need to use `use actix_web::ws::WsWriter`
## Migration from 0.4 to 0.5
* `HttpResponseBuilder::body()`, `.finish()`, `.json()`
methods return `HttpResponse` instead of `Result<HttpResponse>`
* `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
moved to `actix_web::http` module
* `actix_web::header` moved to `actix_web::http::header`
* `NormalizePath` moved to `actix_web::http` module
* `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
shortcut for `actix_web::server::HttpServer::new()`
* `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
* `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead.
* `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
* `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
functions should be used instead
* `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
instead of `Result<_, http::Error>`
* `Application` renamed to a `App`
* `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev`

View File

@ -1,6 +1,6 @@
.PHONY: default build test doc clean
.PHONY: default build test doc book clean
CARGO_FLAGS := --features "$(FEATURES)"
CARGO_FLAGS := --features "$(FEATURES) alpn"
default: test
@ -10,13 +10,5 @@ build:
test: build clippy
cargo test $(CARGO_FLAGS)
skeptic:
USE_SKEPTIC=1 cargo test $(CARGO_FLAGS)
# cd examples/word-count && python setup.py install && pytest -v tests
clippy:
if $$CLIPPY; then cargo clippy $(CARGO_FLAGS); fi
doc: build
cargo doc --no-deps $(CARGO_FLAGS)

182
README.md
View File

@ -1,123 +1,89 @@
# 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](http://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web)
# 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)
Asynchronous web framework for [Actix](https://github.com/actix/actix).
Actix web is a simple, pragmatic and extremely fast web framework for Rust.
* [API Documentation (Development)](http://actix.github.io/actix-web/actix_web/)
* 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/)
* Graceful server shutdown
* Multipart streams
* Static assets
* SSL support with OpenSSL or `native-tls`
* Middlewares ([Logger](https://actix.rs/book/actix-web/sec-9-middlewares.html#logging),
[Session](https://actix.rs/book/actix-web/sec-9-middlewares.html#user-sessions),
[Redis sessions](https://github.com/actix/actix-redis),
[DefaultHeaders](https://actix.rs/book/actix-web/sec-9-middlewares.html#default-headers),
[CORS](https://actix.rs/actix-web/actix_web/middleware/cors/index.html),
[CSRF](https://actix.rs/actix-web/actix_web/middleware/csrf/index.html))
* 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)
## 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.20 or later
---
Actix web is licensed under the [Apache-2.0 license](http://opensource.org/licenses/APACHE-2.0).
## Features
* HTTP 1.1 and 1.0 support
* Streaming and pipelining support
* Keep-alive and slow requests support
* [WebSockets support](https://actix.github.io/actix-web/actix_web/ws/index.html)
* Configurable request routing
* Multipart streams
* Middlewares
## Usage
To use `actix-web`, add this to your `Cargo.toml`:
```toml
[dependencies]
actix-web = "0.2"
```
* Minimum supported Rust version: 1.24 or later
## Example
* [Basic](https://github.com/actix/actix-web/tree/master/examples/basic.rs)
* [Stateful](https://github.com/actix/actix-web/tree/master/examples/state.rs)
* [Mulitpart streams](https://github.com/actix/actix-web/tree/master/examples/multipart)
* [Simple websocket session](https://github.com/actix/actix-web/tree/master/examples/websocket.rs)
* [Tcp/Websocket chat](https://github.com/actix/actix-web/tree/master/examples/websocket-chat)
* [SockJS Server](https://github.com/fafhrd91/actix-sockjs)
```rust
extern crate actix;
extern crate actix_web;
extern crate env_logger;
use actix_web::{http, server, App, Path, Responder};
use actix::*;
use actix_web::*;
struct MyWebSocket;
/// Actor with http context
impl Actor for MyWebSocket {
type Context = HttpContext<Self>;
}
/// Http route handler
impl Route for MyWebSocket {
type State = ();
fn request(req: &mut HttpRequest,
payload: Payload, ctx: &mut HttpContext<Self>) -> RouteResult<Self>
{
// websocket handshake
let resp = ws::handshake(req)?;
// send HttpResponse back to peer
ctx.start(resp);
// convert bytes stream to a stream of `ws::Message` and handle stream
ctx.add_stream(ws::WsStream::new(payload));
Reply::async(MyWebSocket)
}
}
/// Standard actix's stream handler for a stream of `ws::Message`
impl StreamHandler<ws::Message> for MyWebSocket {
fn started(&mut self, ctx: &mut Self::Context) {
println!("WebSocket session openned");
}
fn finished(&mut self, ctx: &mut Self::Context) {
println!("WebSocket session closed");
}
}
impl Handler<ws::Message> for MyWebSocket {
fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext<Self>)
-> Response<Self, ws::Message>
{
// process websocket messages
println!("WS: {:?}", msg);
match msg {
ws::Message::Ping(msg) => ws::WsWriter::pong(ctx, &msg),
ws::Message::Text(text) => ws::WsWriter::text(ctx, &text),
ws::Message::Binary(bin) => ws::WsWriter::binary(ctx, bin),
ws::Message::Closed | ws::Message::Error => {
ctx.stop();
}
_ => (),
}
Self::empty()
}
fn index(info: Path<(u32, String)>) -> impl Responder {
format!("Hello {}! id:{}", info.1, info.0)
}
fn main() {
::std::env::set_var("RUST_LOG", "actix_web=info");
let _ = env_logger::init();
let sys = actix::System::new("ws-example");
HttpServer::new(
Application::default("/")
// enable logger
.middleware(Logger::new(None))
// websocket route
.resource("/ws/", |r| r.get::<MyWebSocket>())
.route_handler("/", StaticFiles::new("examples/static/", true)))
.serve::<_, ()>("127.0.0.1:8080").unwrap();
Arbiter::system().send(msgs::SystemExit(0));
let _ = sys.run();
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-r15&hw=ph&test=plaintext)
* Some basic benchmarks could be found in this [repository](https://github.com/fafhrd91/benchmarks).
## 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

@ -1,18 +1,16 @@
extern crate skeptic;
use std::{env, fs};
extern crate version_check;
#[cfg(unix)]
fn main() {
if env::var("USE_SKEPTIC").is_ok() {
// generates doc tests for `README.md`.
skeptic::generate_doc_tests(&["README.md"]);
} else {
let f = env::var("OUT_DIR").unwrap() + "/skeptic-tests.rs";
let _ = fs::File::create(f);
}
}
#[cfg(not(unix))]
fn main() {
match version_check::is_min_version("1.26.0") {
Some((true, _)) => println!("cargo:rustc-cfg=actix_impl_trait"),
_ => (),
};
match version_check::is_nightly() {
Some(true) => {
println!("cargo:rustc-cfg=actix_nightly");
println!("cargo:rustc-cfg=actix_impl_trait");
}
Some(false) => (),
None => (),
};
}

4
cov.sh
View File

@ -1,4 +0,0 @@
#!/bin/bash
for file in target/debug/actix_web-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; /usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done &&
for file in target/debug/test_*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; /usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done

View File

@ -1,53 +0,0 @@
#![allow(unused_variables)]
extern crate actix;
extern crate actix_web;
extern crate env_logger;
use actix_web::*;
/// somple handle
fn index(req: &mut HttpRequest, _payload: Payload, state: &()) -> HttpResponse {
println!("{:?}", req);
httpcodes::HTTPOk.into()
}
/// handle with path parameters like `/name/{name}/`
fn with_param(req: &mut HttpRequest, _payload: Payload, state: &())
-> HandlerResult<HttpResponse>
{
println!("{:?}", req);
Ok(HttpResponse::builder(StatusCode::OK)
.content_type("test/plain")
.body(format!("Hello {}!", req.match_info().get("name").unwrap()))?)
}
fn main() {
::std::env::set_var("RUST_LOG", "actix_web=info");
let _ = env_logger::init();
let sys = actix::System::new("ws-example");
HttpServer::new(
Application::default("/")
// enable logger
.middleware(Logger::new(None))
// register simple handler, handle all methods
.handler("/index.html", index)
// with path parameters
.resource("/user/{name}/", |r| r.handler(Method::GET, with_param))
// redirect
.resource("/", |r| r.handler(Method::GET, |req, _, _| {
println!("{:?}", req);
Ok(httpcodes::HTTPFound
.builder()
.header("LOCATION", "/index.html")
.body(Body::Empty)?)
}))
// static files
.route_handler("/static", StaticFiles::new("examples/static/", true)))
.serve::<_, ()>("127.0.0.1:8080").unwrap();
println!("Started http server: 127.0.0.1:8080");
let _ = sys.run();
}

View File

@ -1,14 +0,0 @@
[package]
name = "multipart-example"
version = "0.1.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
[[bin]]
name = "multipart"
path = "src/main.rs"
[dependencies]
env_logger = "*"
#actix = "0.3"
actix = { git = "https://github.com/actix/actix.git" }
actix-web = { path = "../../" }

View File

@ -1,32 +0,0 @@
import asyncio
import aiohttp
def req1():
with aiohttp.MultipartWriter() as writer:
writer.append('test')
writer.append_json({'passed': True})
resp = yield from aiohttp.request(
"post", 'http://localhost:8080/multipart',
data=writer, headers=writer.headers)
print(resp)
assert 200 == resp.status
def req2():
with aiohttp.MultipartWriter() as writer:
writer.append('test')
writer.append_json({'passed': True})
writer.append(open('src/main.rs'))
resp = yield from aiohttp.request(
"post", 'http://localhost:8080/multipart',
data=writer, headers=writer.headers)
print(resp)
assert 200 == resp.status
loop = asyncio.get_event_loop()
loop.run_until_complete(req1())
loop.run_until_complete(req2())

View File

@ -1,78 +0,0 @@
#![allow(unused_variables)]
extern crate actix;
extern crate actix_web;
extern crate env_logger;
use actix::*;
use actix_web::*;
struct MyRoute;
impl Actor for MyRoute {
type Context = HttpContext<Self>;
}
impl Route for MyRoute {
type State = ();
fn request(req: &mut HttpRequest, payload: Payload,
ctx: &mut HttpContext<Self>) -> RouteResult<Self> {
println!("{:?}", req);
let multipart = req.multipart(payload)?;
// get Multipart stream
WrapStream::<MyRoute>::actstream(multipart)
.and_then(|item, act, ctx| {
// Multipart stream is a stream of Fields and nested Multiparts
match item {
multipart::MultipartItem::Field(field) => {
println!("==== FIELD ==== {:?}", field);
// Read field's stream
fut::Either::A(
field.actstream()
.map(|chunk, act, ctx| {
println!(
"-- CHUNK: \n{}",
std::str::from_utf8(&chunk.0).unwrap());
})
.finish())
},
multipart::MultipartItem::Nested(mp) => {
// Do nothing for nested multipart stream
fut::Either::B(fut::ok(()))
}
}
})
// wait until stream finish
.finish()
.map_err(|e, act, ctx| {
ctx.start(httpcodes::HTTPBadRequest);
ctx.write_eof();
})
.map(|_, act, ctx| {
ctx.start(httpcodes::HTTPOk);
ctx.write_eof();
})
.spawn(ctx);
Reply::async(MyRoute)
}
}
fn main() {
let _ = env_logger::init();
let sys = actix::System::new("multipart-example");
HttpServer::new(
vec![
Application::default("/")
.resource("/multipart", |r| {
r.post::<MyRoute>();
}).finish()
])
.serve::<_, ()>("127.0.0.1:8080").unwrap();
let _ = sys.run();
}

View File

@ -1,86 +0,0 @@
//! There are two level of statfulness in actix-web. Application has state
//! that is shared across all handlers within same Application.
//! And individual handler can have state.
extern crate actix;
extern crate actix_web;
extern crate env_logger;
use actix::*;
use actix_web::*;
use std::cell::Cell;
struct AppState {
counter: Cell<usize>,
}
/// somple handle
fn index(req: &mut HttpRequest, _: Payload, state: &AppState) -> HttpResponse {
println!("{:?}", req);
state.counter.set(state.counter.get() + 1);
httpcodes::HTTPOk.with_body(
format!("Num of requests: {}", state.counter.get()))
}
/// `MyWebSocket` counts how many messages it receives from peer,
/// websocket-client.py could be used for tests
struct MyWebSocket {
counter: usize,
}
impl Actor for MyWebSocket {
type Context = HttpContext<Self>;
}
impl Route for MyWebSocket {
/// Shared application state
type State = AppState;
fn request(req: &mut HttpRequest,
payload: Payload, ctx: &mut HttpContext<Self>) -> RouteResult<Self>
{
let resp = ws::handshake(req)?;
ctx.start(resp);
ctx.add_stream(ws::WsStream::new(payload));
Reply::async(MyWebSocket{counter: 0})
}
}
impl StreamHandler<ws::Message> for MyWebSocket {}
impl Handler<ws::Message> for MyWebSocket {
fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext<Self>)
-> Response<Self, ws::Message>
{
self.counter += 1;
println!("WS({}): {:?}", self.counter, msg);
match msg {
ws::Message::Ping(msg) => ws::WsWriter::pong(ctx, &msg),
ws::Message::Text(text) => ws::WsWriter::text(ctx, &text),
ws::Message::Binary(bin) => ws::WsWriter::binary(ctx, bin),
ws::Message::Closed | ws::Message::Error => {
ctx.stop();
}
_ => (),
}
Self::empty()
}
}
fn main() {
::std::env::set_var("RUST_LOG", "actix_web=info");
let _ = env_logger::init();
let sys = actix::System::new("ws-example");
HttpServer::new(
Application::builder("/", AppState{counter: Cell::new(0)})
// enable logger
.middleware(Logger::new(None))
// websocket route
.resource("/ws/", |r| r.get::<MyWebSocket>())
// register simple handler, handle all methods
.handler("/", index))
.serve::<_, ()>("127.0.0.1:8080").unwrap();
println!("Started http server: 127.0.0.1:8080");
let _ = sys.run();
}

View File

@ -1,90 +0,0 @@
<!DOCTYPE html>
<meta charset="utf-8" />
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">
</script>
<script language="javascript" type="text/javascript">
$(function() {
var conn = null;
function log(msg) {
var control = $('#log');
control.html(control.html() + msg + '<br/>');
control.scrollTop(control.scrollTop() + 1000);
}
function connect() {
disconnect();
var wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/ws/';
conn = new WebSocket(wsUri);
log('Connecting...');
conn.onopen = function() {
log('Connected.');
update_ui();
};
conn.onmessage = function(e) {
log('Received: ' + e.data);
};
conn.onclose = function() {
log('Disconnected.');
conn = null;
update_ui();
};
}
function disconnect() {
if (conn != null) {
log('Disconnecting...');
conn.close();
conn = null;
update_ui();
}
}
function update_ui() {
var msg = '';
if (conn == null) {
$('#status').text('disconnected');
$('#connect').html('Connect');
} else {
$('#status').text('connected (' + conn.protocol + ')');
$('#connect').html('Disconnect');
}
}
$('#connect').click(function() {
if (conn == null) {
connect();
} else {
disconnect();
}
update_ui();
return false;
});
$('#send').click(function() {
var text = $('#text').val();
log('Sending: ' + text);
conn.send(text);
$('#text').val('').focus();
return false;
});
$('#text').keyup(function(e) {
if (e.keyCode === 13) {
$('#send').click();
return false;
}
});
});
</script>
</head>
<body>
<h3>Chat!</h3>
<div>
<button id="connect">Connect</button>&nbsp;|&nbsp;Status:
<span id="status">disconnected</span>
</div>
<div id="log"
style="width:20em;height:15em;overflow:auto;border:1px solid black">
</div>
<form id="chatform" onsubmit="return false;">
<input id="text" type="text" />
<input id="send" type="button" value="Send" />
</form>
</body>
</html>

View File

@ -1,29 +0,0 @@
[package]
name = "websocket-example"
version = "0.1.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
[[bin]]
name = "server"
path = "src/main.rs"
[[bin]]
name = "client"
path = "src/client.rs"
[dependencies]
rand = "*"
bytes = "0.4"
byteorder = "1.1"
futures = "0.1"
tokio-io = "0.1"
tokio-core = "0.1"
env_logger = "*"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
#actix = "0.3"
actix = { git = "https://github.com/actix/actix.git" }
actix-web = { path = "../../" }

View File

@ -1,35 +0,0 @@
# Websocket chat example
This is extension of the
[actix chat example](https://github.com/actix/actix/tree/master/examples/chat)
Added features:
* Browser WebSocket client
* Chat server runs in separate thread
* Tcp listener runs in separate thread
## Server
Chat server listens for incoming tcp connections. Server can access several types of message:
* `\list` - list all available rooms
* `\join name` - join room, if room does not exist, create new one
* `\name name` - set session name
* `some message` - just string, send messsage to all peers in same room
* client has to send heartbeat `Ping` messages, if server does not receive a heartbeat
message for 10 seconds connection gets droppped
To start server use command: `cargo run --bin server`
## Client
Client connects to server. Reads input from stdin and sends to server.
To run client use command: `cargo run --bin client`
## WebSocket Browser Client
Open url: http://localhost:8080/

View File

@ -1,72 +0,0 @@
#!/usr/bin/env python3
"""websocket cmd client for wssrv.py example."""
import argparse
import asyncio
import signal
import sys
import aiohttp
def start_client(loop, url):
name = input('Please enter your name: ')
# send request
ws = yield from aiohttp.ClientSession().ws_connect(url, autoclose=False, autoping=False)
# input reader
def stdin_callback():
line = sys.stdin.buffer.readline().decode('utf-8')
if not line:
loop.stop()
else:
ws.send_str(name + ': ' + line)
loop.add_reader(sys.stdin.fileno(), stdin_callback)
@asyncio.coroutine
def dispatch():
while True:
msg = yield from ws.receive()
if msg.type == aiohttp.WSMsgType.TEXT:
print('Text: ', msg.data.strip())
elif msg.type == aiohttp.WSMsgType.BINARY:
print('Binary: ', msg.data)
elif msg.type == aiohttp.WSMsgType.PING:
ws.pong()
elif msg.type == aiohttp.WSMsgType.PONG:
print('Pong received')
else:
if msg.type == aiohttp.WSMsgType.CLOSE:
yield from ws.close()
elif msg.type == aiohttp.WSMsgType.ERROR:
print('Error during receive %s' % ws.exception())
elif msg.type == aiohttp.WSMsgType.CLOSED:
pass
break
yield from dispatch()
ARGS = argparse.ArgumentParser(
description="websocket console client for wssrv.py example.")
ARGS.add_argument(
'--host', action="store", dest='host',
default='127.0.0.1', help='Host name')
ARGS.add_argument(
'--port', action="store", dest='port',
default=8080, type=int, help='Port number')
if __name__ == '__main__':
args = ARGS.parse_args()
if ':' in args.host:
args.host, port = args.host.split(':', 1)
args.port = int(port)
url = 'http://{}:{}/ws/'.format(args.host, args.port)
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, loop.stop)
asyncio.Task(start_client(loop, url))
loop.run_forever()

View File

@ -1,161 +0,0 @@
extern crate actix;
extern crate bytes;
extern crate byteorder;
extern crate futures;
extern crate tokio_io;
extern crate tokio_core;
extern crate serde;
extern crate serde_json;
#[macro_use] extern crate serde_derive;
use std::{io, net, process, thread};
use std::str::FromStr;
use std::time::Duration;
use futures::Future;
use tokio_core::net::TcpStream;
use actix::prelude::*;
mod codec;
fn main() {
let sys = actix::System::new("chat-client");
// Connect to server
let addr = net::SocketAddr::from_str("127.0.0.1:12345").unwrap();
Arbiter::handle().spawn(
TcpStream::connect(&addr, Arbiter::handle())
.and_then(|stream| {
let addr: SyncAddress<_> = ChatClient.framed(stream, codec::ClientChatCodec);
// start console loop
thread::spawn(move|| {
loop {
let mut cmd = String::new();
if io::stdin().read_line(&mut cmd).is_err() {
println!("error");
return
}
addr.send(ClientCommand(cmd));
}
});
futures::future::ok(())
})
.map_err(|e| {
println!("Can not connect to server: {}", e);
process::exit(1)
})
);
println!("Running chat client");
sys.run();
}
struct ChatClient;
struct ClientCommand(String);
impl ResponseType for ClientCommand {
type Item = ();
type Error = ();
}
impl Actor for ChatClient {
type Context = FramedContext<Self>;
fn started(&mut self, ctx: &mut FramedContext<Self>) {
// start heartbeats otherwise server will disconnect after 10 seconds
self.hb(ctx)
}
}
impl ChatClient {
fn hb(&self, ctx: &mut FramedContext<Self>) {
ctx.run_later(Duration::new(1, 0), |act, ctx| {
if ctx.send(codec::ChatRequest::Ping).is_ok() {
act.hb(ctx);
}
});
}
}
/// Handle stdin commands
impl Handler<ClientCommand> for ChatClient
{
fn handle(&mut self, msg: ClientCommand, ctx: &mut FramedContext<Self>)
-> Response<Self, ClientCommand>
{
let m = msg.0.trim();
if m.is_empty() {
return Self::empty()
}
// we check for /sss type of messages
if m.starts_with('/') {
let v: Vec<&str> = m.splitn(2, ' ').collect();
match v[0] {
"/list" => {
let _ = ctx.send(codec::ChatRequest::List);
},
"/join" => {
if v.len() == 2 {
let _ = ctx.send(codec::ChatRequest::Join(v[1].to_owned()));
} else {
println!("!!! room name is required");
}
},
_ => println!("!!! unknown command"),
}
} else {
let _ = ctx.send(codec::ChatRequest::Message(m.to_owned()));
}
Self::empty()
}
}
/// Server communication
impl FramedActor for ChatClient {
type Io = TcpStream;
type Codec = codec::ClientChatCodec;
}
impl StreamHandler<codec::ChatResponse, io::Error> for ChatClient {
fn finished(&mut self, _: &mut FramedContext<Self>) {
println!("Disconnected");
// Stop application on disconnect
Arbiter::system().send(msgs::SystemExit(0));
}
}
impl Handler<codec::ChatResponse, io::Error> for ChatClient {
fn handle(&mut self, msg: codec::ChatResponse, _: &mut FramedContext<Self>)
-> Response<Self, codec::ChatResponse>
{
match msg {
codec::ChatResponse::Message(ref msg) => {
println!("message: {}", msg);
}
codec::ChatResponse::Joined(ref msg) => {
println!("!!! joined: {}", msg);
}
codec::ChatResponse::Rooms(rooms) => {
println!("\n!!! Available rooms:");
for room in rooms {
println!("{}", room);
}
println!("");
}
_ => (),
}
Self::empty()
}
}

View File

@ -1,134 +0,0 @@
#![allow(dead_code)]
use std::io;
use serde_json as json;
use byteorder::{BigEndian , ByteOrder};
use bytes::{BytesMut, BufMut};
use tokio_io::codec::{Encoder, Decoder};
use actix::ResponseType;
/// Client request
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag="cmd", content="data")]
pub enum ChatRequest {
/// List rooms
List,
/// Join rooms
Join(String),
/// Send message
Message(String),
/// Ping
Ping
}
impl ResponseType for ChatRequest {
type Item = ();
type Error = ();
}
/// Server response
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag="cmd", content="data")]
pub enum ChatResponse {
Ping,
/// List of rooms
Rooms(Vec<String>),
/// Joined
Joined(String),
/// Message
Message(String),
}
impl ResponseType for ChatResponse {
type Item = ();
type Error = ();
}
/// Codec for Client -> Server transport
pub struct ChatCodec;
impl Decoder for ChatCodec
{
type Item = ChatRequest;
type Error = io::Error;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
let size = {
if src.len() < 2 {
return Ok(None)
}
BigEndian::read_u16(src.as_ref()) as usize
};
if src.len() >= size + 2 {
src.split_to(2);
let buf = src.split_to(size);
Ok(Some(json::from_slice::<ChatRequest>(&buf)?))
} else {
Ok(None)
}
}
}
impl Encoder for ChatCodec
{
type Item = ChatResponse;
type Error = io::Error;
fn encode(&mut self, msg: ChatResponse, dst: &mut BytesMut) -> Result<(), Self::Error> {
let msg = json::to_string(&msg).unwrap();
let msg_ref: &[u8] = msg.as_ref();
dst.reserve(msg_ref.len() + 2);
dst.put_u16::<BigEndian>(msg_ref.len() as u16);
dst.put(msg_ref);
Ok(())
}
}
/// Codec for Server -> Client transport
pub struct ClientChatCodec;
impl Decoder for ClientChatCodec
{
type Item = ChatResponse;
type Error = io::Error;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
let size = {
if src.len() < 2 {
return Ok(None)
}
BigEndian::read_u16(src.as_ref()) as usize
};
if src.len() >= size + 2 {
src.split_to(2);
let buf = src.split_to(size);
Ok(Some(json::from_slice::<ChatResponse>(&buf)?))
} else {
Ok(None)
}
}
}
impl Encoder for ClientChatCodec
{
type Item = ChatRequest;
type Error = io::Error;
fn encode(&mut self, msg: ChatRequest, dst: &mut BytesMut) -> Result<(), Self::Error> {
let msg = json::to_string(&msg).unwrap();
let msg_ref: &[u8] = msg.as_ref();
dst.reserve(msg_ref.len() + 2);
dst.put_u16::<BigEndian>(msg_ref.len() as u16);
dst.put(msg_ref);
Ok(())
}
}

View File

@ -1,226 +0,0 @@
#![allow(unused_variables)]
extern crate rand;
extern crate bytes;
extern crate byteorder;
extern crate futures;
extern crate tokio_io;
extern crate tokio_core;
extern crate env_logger;
extern crate serde;
extern crate serde_json;
#[macro_use] extern crate serde_derive;
extern crate actix;
extern crate actix_web;
use std::time::Instant;
use actix::*;
use actix_web::*;
mod codec;
mod server;
mod session;
/// This is our websocket route state, this state is shared with all route instances
/// via `HttpContext::state()`
struct WsChatSessionState {
addr: SyncAddress<server::ChatServer>,
}
struct WsChatSession {
/// unique session id
id: usize,
/// Client must send ping at least once per 10 seconds, otherwise we drop connection.
hb: Instant,
/// joined room
room: String,
/// peer name
name: Option<String>,
}
impl Actor for WsChatSession {
type Context = HttpContext<Self>;
}
/// Entry point for our route
impl Route for WsChatSession {
type State = WsChatSessionState;
fn request(req: &mut HttpRequest,
payload: Payload, ctx: &mut HttpContext<Self>) -> RouteResult<Self>
{
// websocket handshakre, it may fail if request is not websocket request
let resp = ws::handshake(&req)?;
ctx.start(resp);
ctx.add_stream(ws::WsStream::new(payload));
Reply::async(
WsChatSession {
id: 0,
hb: Instant::now(),
room: "Main".to_owned(),
name: None})
}
}
/// Handle messages from chat server, we simply send it to peer websocket
impl Handler<session::Message> for WsChatSession {
fn handle(&mut self, msg: session::Message, ctx: &mut HttpContext<Self>)
-> Response<Self, session::Message>
{
ws::WsWriter::text(ctx, &msg.0);
Self::empty()
}
}
/// WebSocket message handler
impl Handler<ws::Message> for WsChatSession {
fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext<Self>)
-> Response<Self, ws::Message>
{
println!("WEBSOCKET MESSAGE: {:?}", msg);
match msg {
ws::Message::Ping(msg) =>
ws::WsWriter::pong(ctx, &msg),
ws::Message::Pong(msg) =>
self.hb = Instant::now(),
ws::Message::Text(text) => {
let m = text.trim();
// we check for /sss type of messages
if m.starts_with('/') {
let v: Vec<&str> = m.splitn(2, ' ').collect();
match v[0] {
"/list" => {
// Send ListRooms message to chat server and wait for response
println!("List rooms");
ctx.state().addr.call(self, server::ListRooms).then(|res, _, ctx| {
match res {
Ok(Ok(rooms)) => {
for room in rooms {
ws::WsWriter::text(ctx, &room);
}
},
_ => println!("Something is wrong"),
}
fut::ok(())
}).wait(ctx)
// .wait(ctx) pauses all events in context,
// so actor wont receive any new messages until it get list
// of rooms back
},
"/join" => {
if v.len() == 2 {
self.room = v[1].to_owned();
ctx.state().addr.send(
server::Join{id: self.id, name: self.room.clone()});
ws::WsWriter::text(ctx, "joined");
} else {
ws::WsWriter::text(ctx, "!!! room name is required");
}
},
"/name" => {
if v.len() == 2 {
self.name = Some(v[1].to_owned());
} else {
ws::WsWriter::text(ctx, "!!! name is required");
}
},
_ => ws::WsWriter::text(
ctx, &format!("!!! unknown command: {:?}", m)),
}
} else {
let msg = if let Some(ref name) = self.name {
format!("{}: {}", name, m)
} else {
m.to_owned()
};
// send message to chat server
ctx.state().addr.send(
server::Message{id: self.id,
msg: msg,
room: self.room.clone()})
}
},
ws::Message::Binary(bin) =>
println!("Unexpected binary"),
ws::Message::Closed | ws::Message::Error => {
ctx.stop();
}
_ => (),
}
Self::empty()
}
}
impl StreamHandler<ws::Message> for WsChatSession
{
/// Method is called when stream get polled first time.
/// We register ws session with ChatServer
fn started(&mut self, ctx: &mut Self::Context) {
// register self in chat server. `AsyncContext::wait` register
// future within context, but context waits until this future resolves
// before processing any other events.
// HttpContext::state() is instance of WsChatSessionState, state is shared across all
// routes within application
let subs = ctx.sync_subscriber();
ctx.state().addr.call(
self, server::Connect{addr: subs}).then(
|res, act, ctx| {
match res {
Ok(Ok(res)) => act.id = res,
// something is wrong with chat server
_ => ctx.stop(),
}
fut::ok(())
}).wait(ctx);
}
/// Method is called when stream finishes, even if stream finishes with error.
fn finished(&mut self, ctx: &mut Self::Context) {
// notify chat server
ctx.state().addr.send(server::Disconnect{id: self.id});
ctx.stop()
}
}
fn main() {
let _ = env_logger::init();
let sys = actix::System::new("websocket-example");
// Start chat server actor in separate thread
let server: SyncAddress<_> =
Arbiter::start(|_| server::ChatServer::default());
// Start tcp server in separate thread
let srv = server.clone();
Arbiter::new("tcp-server").send::<msgs::Execute>(
msgs::Execute::new(move || {
session::TcpServer::new("127.0.0.1:12345", srv);
Ok(())
}));
// Websocket sessions state
let state = WsChatSessionState { addr: server };
// Create Http server with websocket support
HttpServer::new(
Application::builder("/", state)
// redirect to websocket.html
.resource("/", |r|
r.handler(Method::GET, |req, payload, state| {
Ok(httpcodes::HTTPFound
.builder()
.header("LOCATION", "/static/websocket.html")
.body(Body::Empty)?)
}))
// websocket
.resource("/ws/", |r| r.get::<WsChatSession>())
// static resources
.route_handler("/static", StaticFiles::new("static/", true)))
.serve::<_, ()>("127.0.0.1:8080").unwrap();
let _ = sys.run();
}

View File

@ -1,217 +0,0 @@
//! `ChatServer` is an actor. It maintains list of connection client session.
//! And manages available rooms. Peers send messages to other peers in same
//! room through `ChatServer`.
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use rand::{self, Rng, ThreadRng};
use actix::prelude::*;
use session;
/// Message for chat server communications
/// New chat session is created
pub struct Connect {
pub addr: Box<Subscriber<session::Message> + Send>,
}
/// Response type for Connect message
///
/// Chat server returns unique session id
impl ResponseType for Connect {
type Item = usize;
type Error = ();
}
/// Session is disconnected
pub struct Disconnect {
pub id: usize,
}
impl ResponseType for Disconnect {
type Item = ();
type Error = ();
}
/// Send message to specific room
pub struct Message {
/// Id of the client session
pub id: usize,
/// Peer message
pub msg: String,
/// Room name
pub room: String,
}
impl ResponseType for Message {
type Item = ();
type Error = ();
}
/// List of available rooms
pub struct ListRooms;
impl ResponseType for ListRooms {
type Item = Vec<String>;
type Error = ();
}
/// Join room, if room does not exists create new one.
pub struct Join {
/// Client id
pub id: usize,
/// Room name
pub name: String,
}
impl ResponseType for Join {
type Item = ();
type Error = ();
}
/// `ChatServer` manages chat rooms and responsible for coordinating chat session.
/// implementation is super primitive
pub struct ChatServer {
sessions: HashMap<usize, Box<Subscriber<session::Message> + Send>>,
rooms: HashMap<String, HashSet<usize>>,
rng: RefCell<ThreadRng>,
}
impl Default for ChatServer {
fn default() -> ChatServer {
// default room
let mut rooms = HashMap::new();
rooms.insert("Main".to_owned(), HashSet::new());
ChatServer {
sessions: HashMap::new(),
rooms: rooms,
rng: RefCell::new(rand::thread_rng()),
}
}
}
impl ChatServer {
/// Send message to all users in the room
fn send_message(&self, room: &str, message: &str, skip_id: usize) {
if let Some(sessions) = self.rooms.get(room) {
for id in sessions {
if *id != skip_id {
if let Some(addr) = self.sessions.get(id) {
let _ = addr.send(session::Message(message.to_owned()));
}
}
}
}
}
}
/// Make actor from `ChatServer`
impl Actor for ChatServer {
/// We are going to use simple Context, we just need ability to communicate
/// with other actors.
type Context = Context<Self>;
}
/// Handler for Connect message.
///
/// Register new session and assign unique id to this session
impl Handler<Connect> for ChatServer {
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Response<Self, Connect> {
println!("Someone joined");
// notify all users in same room
self.send_message(&"Main".to_owned(), "Someone joined", 0);
// register session with random id
let id = self.rng.borrow_mut().gen::<usize>();
self.sessions.insert(id, msg.addr);
// auto join session to Main room
self.rooms.get_mut(&"Main".to_owned()).unwrap().insert(id);
// send id back
Self::reply(id)
}
}
/// Handler for Disconnect message.
impl Handler<Disconnect> for ChatServer {
fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) -> Response<Self, Disconnect> {
println!("Someone disconnected");
let mut rooms: Vec<String> = Vec::new();
// remove address
if self.sessions.remove(&msg.id).is_some() {
// remove session from all rooms
for (name, sessions) in &mut self.rooms {
if sessions.remove(&msg.id) {
rooms.push(name.to_owned());
}
}
}
// send message to other users
for room in rooms {
self.send_message(&room, "Someone disconnected", 0);
}
Self::empty()
}
}
/// Handler for Message message.
impl Handler<Message> for ChatServer {
fn handle(&mut self, msg: Message, _: &mut Context<Self>) -> Response<Self, Message> {
self.send_message(&msg.room, msg.msg.as_str(), msg.id);
Self::empty()
}
}
/// Handler for `ListRooms` message.
impl Handler<ListRooms> for ChatServer {
fn handle(&mut self, _: ListRooms, _: &mut Context<Self>) -> Response<Self, ListRooms> {
let mut rooms = Vec::new();
for key in self.rooms.keys() {
rooms.push(key.to_owned())
}
Self::reply(rooms)
}
}
/// Join room, send disconnect message to old room
/// send join message to new room
impl Handler<Join> for ChatServer {
fn handle(&mut self, msg: Join, _: &mut Context<Self>) -> Response<Self, Join> {
let Join {id, name} = msg;
let mut rooms = Vec::new();
// remove session from all rooms
for (n, sessions) in &mut self.rooms {
if sessions.remove(&id) {
rooms.push(n.to_owned());
}
}
// send message to other users
for room in rooms {
self.send_message(&room, "Someone disconnected", 0);
}
if self.rooms.get_mut(&name).is_none() {
self.rooms.insert(name.clone(), HashSet::new());
}
self.send_message(&name, "Someone connected", id);
self.rooms.get_mut(&name).unwrap().insert(id);
Self::empty()
}
}

View File

@ -1,226 +0,0 @@
//! `ClientSession` is an actor, it manages peer tcp connection and
//! proxies commands from peer to `ChatServer`.
use std::{io, net};
use std::str::FromStr;
use std::time::{Instant, Duration};
use futures::Stream;
use tokio_core::net::{TcpStream, TcpListener};
use actix::*;
use server::{self, ChatServer};
use codec::{ChatRequest, ChatResponse, ChatCodec};
/// Chat server sends this messages to session
pub struct Message(pub String);
impl ResponseType for Message {
type Item = ();
type Error = ();
}
/// `ChatSession` actor is responsible for tcp peer communitions.
pub struct ChatSession {
/// unique session id
id: usize,
/// this is address of chat server
addr: SyncAddress<ChatServer>,
/// Client must send ping at least once per 10 seconds, otherwise we drop connection.
hb: Instant,
/// joined room
room: String,
}
impl Actor for ChatSession {
/// For tcp communication we are going to use `FramedContext`.
/// It is convinient wrapper around `Framed` object from `tokio_io`
type Context = FramedContext<Self>;
}
/// To use `FramedContext` we have to define Io type and Codec
impl FramedActor for ChatSession {
type Io = TcpStream;
type Codec= ChatCodec;
}
/// Also `FramedContext` requires Actor which is able to handle stream
/// of `<Codec as Decoder>::Item` items.
impl StreamHandler<ChatRequest, io::Error> for ChatSession {
fn started(&mut self, ctx: &mut FramedContext<Self>) {
// we'll start heartbeat process on session start.
self.hb(ctx);
// register self in chat server. `AsyncContext::wait` register
// future within context, but context waits until this future resolves
// before processing any other events.
self.addr.call(self, server::Connect{addr: ctx.sync_subscriber()}).then(|res, act, ctx| {
match res {
Ok(Ok(res)) => act.id = res,
// something is wrong with chat server
_ => ctx.stop(),
}
fut::ok(())
}).wait(ctx);
}
fn finished(&mut self, ctx: &mut FramedContext<Self>) {
// notify chat server
self.addr.send(server::Disconnect{id: self.id});
ctx.stop()
}
}
impl Handler<ChatRequest, io::Error> for ChatSession {
/// We'll stop chat session actor on any error, high likely it is just
/// termination of the tcp stream.
fn error(&mut self, _: io::Error, ctx: &mut FramedContext<Self>) {
ctx.stop()
}
/// This is main event loop for client requests
fn handle(&mut self, msg: ChatRequest, ctx: &mut FramedContext<Self>)
-> Response<Self, ChatRequest>
{
match msg {
ChatRequest::List => {
// Send ListRooms message to chat server and wait for response
println!("List rooms");
self.addr.call(self, server::ListRooms).then(|res, _, ctx| {
match res {
Ok(Ok(rooms)) => {
let _ = ctx.send(ChatResponse::Rooms(rooms));
},
_ => println!("Something is wrong"),
}
fut::ok(())
}).wait(ctx)
// .wait(ctx) pauses all events in context,
// so actor wont receive any new messages until it get list of rooms back
},
ChatRequest::Join(name) => {
println!("Join to room: {}", name);
self.room = name.clone();
self.addr.send(server::Join{id: self.id, name: name.clone()});
let _ = ctx.send(ChatResponse::Joined(name));
},
ChatRequest::Message(message) => {
// send message to chat server
println!("Peer message: {}", message);
self.addr.send(
server::Message{id: self.id,
msg: message, room:
self.room.clone()})
}
// we update heartbeat time on ping from peer
ChatRequest::Ping =>
self.hb = Instant::now(),
}
Self::empty()
}
}
/// Handler for Message, chat server sends this message, we just send string to peer
impl Handler<Message> for ChatSession {
fn handle(&mut self, msg: Message, ctx: &mut FramedContext<Self>)
-> Response<Self, Message>
{
// send message to peer
let _ = ctx.send(ChatResponse::Message(msg.0));
Self::empty()
}
}
/// Helper methods
impl ChatSession {
pub fn new(addr: SyncAddress<ChatServer>) -> ChatSession {
ChatSession {id: 0, addr: addr, hb: Instant::now(), room: "Main".to_owned()}
}
/// helper method that sends ping to client every second.
///
/// also this method check heartbeats from client
fn hb(&self, ctx: &mut FramedContext<Self>) {
ctx.run_later(Duration::new(1, 0), |act, ctx| {
// check client heartbeats
if Instant::now().duration_since(act.hb) > Duration::new(10, 0) {
// heartbeat timed out
println!("Client heartbeat failed, disconnecting!");
// notify chat server
act.addr.send(server::Disconnect{id: act.id});
// stop actor
ctx.stop();
}
if ctx.send(ChatResponse::Ping).is_ok() {
// if we can not send message to sink, sink is closed (disconnected)
act.hb(ctx);
}
});
}
}
/// Define tcp server that will accept incomint tcp connection and create
/// chat actors.
pub struct TcpServer {
chat: SyncAddress<ChatServer>,
}
impl TcpServer {
pub fn new(s: &str, chat: SyncAddress<ChatServer>) {
// Create server listener
let addr = net::SocketAddr::from_str("127.0.0.1:12345").unwrap();
let listener = TcpListener::bind(&addr, Arbiter::handle()).unwrap();
// Our chat server `Server` is an actor, first we need to start it
// and then add stream on incoming tcp connections to it.
// TcpListener::incoming() returns stream of the (TcpStream, net::SocketAddr) items
// So to be able to handle this events `Server` actor has to implement
// stream handler `StreamHandler<(TcpStream, net::SocketAddr), io::Error>`
let _: () = TcpServer::create(|ctx| {
ctx.add_stream(listener.incoming().map(|(t, a)| TcpConnect(t, a)));
TcpServer{chat: chat}
});
}
}
/// Make actor from `Server`
impl Actor for TcpServer {
/// Every actor has to provide execution `Context` in which it can run.
type Context = Context<Self>;
}
struct TcpConnect(TcpStream, net::SocketAddr);
impl ResponseType for TcpConnect {
type Item = ();
type Error = ();
}
/// Handle stream of TcpStream's
impl StreamHandler<TcpConnect, io::Error> for TcpServer {}
impl Handler<TcpConnect, io::Error> for TcpServer {
fn handle(&mut self, msg: TcpConnect, _: &mut Context<Self>) -> Response<Self, TcpConnect>
{
// For each incoming connection we create `ChatSession` actor
// with out chat server address.
let server = self.chat.clone();
let _: () = ChatSession::new(server).framed(msg.0, ChatCodec);
// this is response for message, which is defined by `ResponseType` trait
// in this case we just return unit.
Self::empty()
}
}

View File

@ -1,90 +0,0 @@
<!DOCTYPE html>
<meta charset="utf-8" />
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">
</script>
<script language="javascript" type="text/javascript">
$(function() {
var conn = null;
function log(msg) {
var control = $('#log');
control.html(control.html() + msg + '<br/>');
control.scrollTop(control.scrollTop() + 1000);
}
function connect() {
disconnect();
var wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/ws/';
conn = new WebSocket(wsUri);
log('Connecting...');
conn.onopen = function() {
log('Connected.');
update_ui();
};
conn.onmessage = function(e) {
log('Received: ' + e.data);
};
conn.onclose = function() {
log('Disconnected.');
conn = null;
update_ui();
};
}
function disconnect() {
if (conn != null) {
log('Disconnecting...');
conn.close();
conn = null;
update_ui();
}
}
function update_ui() {
var msg = '';
if (conn == null) {
$('#status').text('disconnected');
$('#connect').html('Connect');
} else {
$('#status').text('connected (' + conn.protocol + ')');
$('#connect').html('Disconnect');
}
}
$('#connect').click(function() {
if (conn == null) {
connect();
} else {
disconnect();
}
update_ui();
return false;
});
$('#send').click(function() {
var text = $('#text').val();
log('Sending: ' + text);
conn.send(text);
$('#text').val('').focus();
return false;
});
$('#text').keyup(function(e) {
if (e.keyCode === 13) {
$('#send').click();
return false;
}
});
});
</script>
</head>
<body>
<h3>Chat!</h3>
<div>
<button id="connect">Connect</button>&nbsp;|&nbsp;Status:
<span id="status">disconnected</span>
</div>
<div id="log"
style="width:20em;height:15em;overflow:auto;border:1px solid black">
</div>
<form id="chatform" onsubmit="return false;">
<input id="text" type="text" />
<input id="send" type="button" value="Send" />
</form>
</body>
</html>

View File

@ -1,72 +0,0 @@
#!/usr/bin/env python3
"""websocket cmd client for wssrv.py example."""
import argparse
import asyncio
import signal
import sys
import aiohttp
def start_client(loop, url):
name = input('Please enter your name: ')
# send request
ws = yield from aiohttp.ClientSession().ws_connect(url, autoclose=False, autoping=False)
# input reader
def stdin_callback():
line = sys.stdin.buffer.readline().decode('utf-8')
if not line:
loop.stop()
else:
ws.send_str(name + ': ' + line)
loop.add_reader(sys.stdin.fileno(), stdin_callback)
@asyncio.coroutine
def dispatch():
while True:
msg = yield from ws.receive()
if msg.type == aiohttp.WSMsgType.TEXT:
print('Text: ', msg.data.strip())
elif msg.type == aiohttp.WSMsgType.BINARY:
print('Binary: ', msg.data)
elif msg.type == aiohttp.WSMsgType.PING:
ws.pong()
elif msg.type == aiohttp.WSMsgType.PONG:
print('Pong received')
else:
if msg.type == aiohttp.WSMsgType.CLOSE:
yield from ws.close()
elif msg.type == aiohttp.WSMsgType.ERROR:
print('Error during receive %s' % ws.exception())
elif msg.type == aiohttp.WSMsgType.CLOSED:
pass
break
yield from dispatch()
ARGS = argparse.ArgumentParser(
description="websocket console client for wssrv.py example.")
ARGS.add_argument(
'--host', action="store", dest='host',
default='127.0.0.1', help='Host name')
ARGS.add_argument(
'--port', action="store", dest='port',
default=8080, type=int, help='Port number')
if __name__ == '__main__':
args = ARGS.parse_args()
if ':' in args.host:
args.host, port = args.host.split(':', 1)
args.port = int(port)
url = 'http://{}:{}/ws/'.format(args.host, args.port)
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, loop.stop)
asyncio.Task(start_client(loop, url))
loop.run_forever()

View File

@ -1,85 +0,0 @@
//! Simple echo websocket server.
//! Open `http://localhost:8080/ws/index.html` in browser
//! or [python console client](https://github.com/actix/actix-web/blob/master/examples/websocket-client.py)
//! could be used for testing.
#![allow(unused_variables)]
extern crate actix;
extern crate actix_web;
extern crate env_logger;
use actix::*;
use actix_web::*;
struct MyWebSocket;
impl Actor for MyWebSocket {
type Context = HttpContext<Self>;
}
/// Http route handler
impl Route for MyWebSocket {
type State = ();
fn request(req: &mut HttpRequest,
payload: Payload, ctx: &mut HttpContext<Self>) -> RouteResult<Self>
{
// websocket handshake
let resp = ws::handshake(req)?;
// send HttpResponse back to peer
ctx.start(resp);
// convert bytes stream to a stream of `ws::Message` and register it
ctx.add_stream(ws::WsStream::new(payload));
Reply::async(MyWebSocket)
}
}
/// Standard actix's stream handler for a stream of `ws::Message`
impl StreamHandler<ws::Message> for MyWebSocket {
fn started(&mut self, ctx: &mut Self::Context) {
println!("WebSocket session openned");
}
fn finished(&mut self, ctx: &mut Self::Context) {
println!("WebSocket session closed");
}
}
impl Handler<ws::Message> for MyWebSocket {
fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext<Self>)
-> Response<Self, ws::Message>
{
// process websocket messages
println!("WS: {:?}", msg);
match msg {
ws::Message::Ping(msg) => ws::WsWriter::pong(ctx, &msg),
ws::Message::Text(text) => ws::WsWriter::text(ctx, &text),
ws::Message::Binary(bin) => ws::WsWriter::binary(ctx, bin),
ws::Message::Closed | ws::Message::Error => {
ctx.stop();
}
_ => (),
}
Self::empty()
}
}
fn main() {
::std::env::set_var("RUST_LOG", "actix_web=info");
let _ = env_logger::init();
let sys = actix::System::new("ws-example");
HttpServer::new(
Application::default("/")
// enable logger
.middleware(Logger::new(None))
// websocket route
.resource("/ws/", |r| r.get::<MyWebSocket>())
.route_handler("/", StaticFiles::new("examples/static/", true)))
// start http server on 127.0.0.1:8080
.serve::<_, ()>("127.0.0.1:8080").unwrap();
println!("Started http server: 127.0.0.1:8080");
let _ = sys.run();
}

5
rustfmt.toml Normal file
View File

@ -0,0 +1,5 @@
max_width = 89
reorder_imports = true
wrap_comments = true
fn_args_density = "Compressed"
#use_small_heuristics = false

File diff suppressed because it is too large Load Diff

View File

@ -1,261 +1,385 @@
use bytes::{Bytes, BytesMut};
use futures::Stream;
use std::rc::Rc;
use std::sync::Arc;
use bytes::{Bytes, BytesMut};
use std::{fmt, mem};
use context::ActorHttpContext;
use error::Error;
use handler::Responder;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
/// Type represent streaming body
pub type BodyStream = Box<Stream<Item = Bytes, Error = Error>>;
/// Represents various types of http message body.
#[derive(Debug)]
pub enum Body {
/// Empty response. `Content-Length` header is set to `0`
Empty,
/// Specific response body.
Binary(BinaryBody),
/// Streaming response body with specified length.
Length(u64),
Binary(Binary),
/// Unspecified streaming response. Developer is responsible for setting
/// right `Content-Length` or `Transfer-Encoding` headers.
Streaming,
/// Upgrade connection.
Upgrade,
Streaming(BodyStream),
/// Special body type for actor response.
Actor(Box<ActorHttpContext>),
}
/// Represents various types of binary body.
/// `Content-Length` header is set to length of the body.
#[derive(Debug)]
pub enum BinaryBody {
#[derive(Debug, PartialEq)]
pub enum Binary {
/// Bytes body
Bytes(Bytes),
/// Static slice
Slice(&'static [u8]),
/// Shared bytes body
SharedBytes(Rc<Bytes>),
/// Shared stirng body
/// Shared string body
SharedString(Rc<String>),
/// Shared bytes body
#[doc(hidden)]
ArcSharedBytes(Arc<Bytes>),
/// Shared string body
#[doc(hidden)]
ArcSharedString(Arc<String>),
/// Shared vec body
SharedVec(Arc<Vec<u8>>),
}
impl Body {
/// Does this body have payload.
pub fn has_body(&self) -> bool {
/// Does this body streaming.
#[inline]
pub fn is_streaming(&self) -> bool {
match *self {
Body::Length(_) | Body::Streaming => true,
_ => false
Body::Streaming(_) | Body::Actor(_) => true,
_ => false,
}
}
/// Is this binary body.
#[inline]
pub fn is_binary(&self) -> bool {
match *self {
Body::Binary(_) => true,
_ => false,
}
}
/// Is this binary empy.
#[inline]
pub fn is_empty(&self) -> bool {
match *self {
Body::Empty => true,
_ => false,
}
}
/// Create body from slice (copy)
pub fn from_slice(s: &[u8]) -> Body {
Body::Binary(BinaryBody::Bytes(Bytes::from(s)))
Body::Binary(Binary::Bytes(Bytes::from(s)))
}
/// Is this binary body.
#[inline]
pub(crate) fn binary(self) -> Binary {
match self {
Body::Binary(b) => b,
_ => panic!(),
}
}
}
impl<T> From<T> for Body where T: Into<BinaryBody>{
impl PartialEq for Body {
fn eq(&self, other: &Body) -> bool {
match *self {
Body::Empty => match *other {
Body::Empty => true,
_ => false,
},
Body::Binary(ref b) => match *other {
Body::Binary(ref b2) => b == b2,
_ => false,
},
Body::Streaming(_) | Body::Actor(_) => false,
}
}
}
impl fmt::Debug for Body {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Body::Empty => write!(f, "Body::Empty"),
Body::Binary(ref b) => write!(f, "Body::Binary({:?})", b),
Body::Streaming(_) => write!(f, "Body::Streaming(_)"),
Body::Actor(_) => write!(f, "Body::Actor(_)"),
}
}
}
impl<T> From<T> for Body
where
T: Into<Binary>,
{
fn from(b: T) -> Body {
Body::Binary(b.into())
}
}
impl BinaryBody {
impl From<Box<ActorHttpContext>> for Body {
fn from(ctx: Box<ActorHttpContext>) -> Body {
Body::Actor(ctx)
}
}
impl Binary {
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline]
pub fn len(&self) -> usize {
match *self {
BinaryBody::Bytes(ref bytes) => bytes.len(),
BinaryBody::Slice(slice) => slice.len(),
BinaryBody::SharedBytes(ref bytes) => bytes.len(),
BinaryBody::ArcSharedBytes(ref bytes) => bytes.len(),
BinaryBody::SharedString(ref s) => s.len(),
BinaryBody::ArcSharedString(ref s) => s.len(),
Binary::Bytes(ref bytes) => bytes.len(),
Binary::Slice(slice) => slice.len(),
Binary::SharedString(ref s) => s.len(),
Binary::ArcSharedString(ref s) => s.len(),
Binary::SharedVec(ref s) => s.len(),
}
}
/// Create binary body from slice
pub fn from_slice(s: &[u8]) -> BinaryBody {
BinaryBody::Bytes(Bytes::from(s))
pub fn from_slice(s: &[u8]) -> Binary {
Binary::Bytes(Bytes::from(s))
}
/// Convert Binary to a Bytes instance
pub fn take(&mut self) -> Bytes {
mem::replace(self, Binary::Slice(b"")).into()
}
}
impl From<&'static str> for BinaryBody {
fn from(s: &'static str) -> BinaryBody {
BinaryBody::Slice(s.as_ref())
}
}
impl From<&'static [u8]> for BinaryBody {
fn from(s: &'static [u8]) -> BinaryBody {
BinaryBody::Slice(s)
}
}
impl From<Vec<u8>> for BinaryBody {
fn from(vec: Vec<u8>) -> BinaryBody {
BinaryBody::Bytes(Bytes::from(vec))
}
}
impl From<String> for BinaryBody {
fn from(s: String) -> BinaryBody {
BinaryBody::Bytes(Bytes::from(s))
}
}
impl<'a> From<&'a String> for BinaryBody {
fn from(s: &'a String) -> BinaryBody {
BinaryBody::Bytes(Bytes::from(AsRef::<[u8]>::as_ref(&s)))
}
}
impl From<Bytes> for BinaryBody {
fn from(s: Bytes) -> BinaryBody {
BinaryBody::Bytes(s)
}
}
impl From<BytesMut> for BinaryBody {
fn from(s: BytesMut) -> BinaryBody {
BinaryBody::Bytes(s.freeze())
}
}
impl From<Rc<Bytes>> for BinaryBody {
fn from(body: Rc<Bytes>) -> BinaryBody {
BinaryBody::SharedBytes(body)
}
}
impl<'a> From<&'a Rc<Bytes>> for BinaryBody {
fn from(body: &'a Rc<Bytes>) -> BinaryBody {
BinaryBody::SharedBytes(Rc::clone(body))
}
}
impl From<Arc<Bytes>> for BinaryBody {
fn from(body: Arc<Bytes>) -> BinaryBody {
BinaryBody::ArcSharedBytes(body)
}
}
impl<'a> From<&'a Arc<Bytes>> for BinaryBody {
fn from(body: &'a Arc<Bytes>) -> BinaryBody {
BinaryBody::ArcSharedBytes(Arc::clone(body))
}
}
impl From<Rc<String>> for BinaryBody {
fn from(body: Rc<String>) -> BinaryBody {
BinaryBody::SharedString(body)
}
}
impl<'a> From<&'a Rc<String>> for BinaryBody {
fn from(body: &'a Rc<String>) -> BinaryBody {
BinaryBody::SharedString(Rc::clone(body))
}
}
impl From<Arc<String>> for BinaryBody {
fn from(body: Arc<String>) -> BinaryBody {
BinaryBody::ArcSharedString(body)
}
}
impl<'a> From<&'a Arc<String>> for BinaryBody {
fn from(body: &'a Arc<String>) -> BinaryBody {
BinaryBody::ArcSharedString(Arc::clone(body))
}
}
impl AsRef<[u8]> for BinaryBody {
fn as_ref(&self) -> &[u8] {
impl Clone for Binary {
fn clone(&self) -> Binary {
match *self {
BinaryBody::Bytes(ref bytes) => bytes.as_ref(),
BinaryBody::Slice(slice) => slice,
BinaryBody::SharedBytes(ref bytes) => bytes.as_ref(),
BinaryBody::ArcSharedBytes(ref bytes) => bytes.as_ref(),
BinaryBody::SharedString(ref s) => s.as_bytes(),
BinaryBody::ArcSharedString(ref s) => s.as_bytes(),
Binary::Bytes(ref bytes) => Binary::Bytes(bytes.clone()),
Binary::Slice(slice) => Binary::Bytes(Bytes::from(slice)),
Binary::SharedString(ref s) => Binary::SharedString(s.clone()),
Binary::ArcSharedString(ref s) => Binary::ArcSharedString(s.clone()),
Binary::SharedVec(ref s) => Binary::SharedVec(s.clone()),
}
}
}
impl Into<Bytes> for Binary {
fn into(self) -> Bytes {
match self {
Binary::Bytes(bytes) => bytes,
Binary::Slice(slice) => Bytes::from(slice),
Binary::SharedString(s) => Bytes::from(s.as_str()),
Binary::ArcSharedString(s) => Bytes::from(s.as_str()),
Binary::SharedVec(s) => Bytes::from(AsRef::<[u8]>::as_ref(s.as_ref())),
}
}
}
impl From<&'static str> for Binary {
fn from(s: &'static str) -> Binary {
Binary::Slice(s.as_ref())
}
}
impl From<&'static [u8]> for Binary {
fn from(s: &'static [u8]) -> Binary {
Binary::Slice(s)
}
}
impl From<Vec<u8>> for Binary {
fn from(vec: Vec<u8>) -> Binary {
Binary::Bytes(Bytes::from(vec))
}
}
impl From<String> for Binary {
fn from(s: String) -> Binary {
Binary::Bytes(Bytes::from(s))
}
}
impl<'a> From<&'a String> for Binary {
fn from(s: &'a String) -> Binary {
Binary::Bytes(Bytes::from(AsRef::<[u8]>::as_ref(&s)))
}
}
impl From<Bytes> for Binary {
fn from(s: Bytes) -> Binary {
Binary::Bytes(s)
}
}
impl From<BytesMut> for Binary {
fn from(s: BytesMut) -> Binary {
Binary::Bytes(s.freeze())
}
}
impl From<Rc<String>> for Binary {
fn from(body: Rc<String>) -> Binary {
Binary::SharedString(body)
}
}
impl<'a> From<&'a Rc<String>> for Binary {
fn from(body: &'a Rc<String>) -> Binary {
Binary::SharedString(Rc::clone(body))
}
}
impl From<Arc<String>> for Binary {
fn from(body: Arc<String>) -> Binary {
Binary::ArcSharedString(body)
}
}
impl<'a> From<&'a Arc<String>> for Binary {
fn from(body: &'a Arc<String>) -> Binary {
Binary::ArcSharedString(Arc::clone(body))
}
}
impl From<Arc<Vec<u8>>> for Binary {
fn from(body: Arc<Vec<u8>>) -> Binary {
Binary::SharedVec(body)
}
}
impl<'a> From<&'a Arc<Vec<u8>>> for Binary {
fn from(body: &'a Arc<Vec<u8>>) -> Binary {
Binary::SharedVec(Arc::clone(body))
}
}
impl AsRef<[u8]> for Binary {
#[inline]
fn as_ref(&self) -> &[u8] {
match *self {
Binary::Bytes(ref bytes) => bytes.as_ref(),
Binary::Slice(slice) => slice,
Binary::SharedString(ref s) => s.as_bytes(),
Binary::ArcSharedString(ref s) => s.as_bytes(),
Binary::SharedVec(ref s) => s.as_ref().as_ref(),
}
}
}
impl Responder for Binary {
type Item = HttpResponse;
type Error = Error;
fn respond_to<S>(self, req: &HttpRequest<S>) -> Result<HttpResponse, Error> {
Ok(HttpResponse::build_from(req)
.content_type("application/octet-stream")
.body(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_body_is_streaming() {
assert_eq!(Body::Empty.is_streaming(), false);
assert_eq!(Body::Binary(Binary::from("")).is_streaming(), false);
}
#[test]
fn test_is_empty() {
assert_eq!(Binary::from("").is_empty(), true);
assert_eq!(Binary::from("test").is_empty(), false);
}
#[test]
fn test_static_str() {
assert_eq!(BinaryBody::from("test").len(), 4);
assert_eq!(BinaryBody::from("test").as_ref(), "test".as_bytes());
assert_eq!(Binary::from("test").len(), 4);
assert_eq!(Binary::from("test").as_ref(), b"test");
}
#[test]
fn test_static_bytes() {
assert_eq!(BinaryBody::from(b"test".as_ref()).len(), 4);
assert_eq!(BinaryBody::from(b"test".as_ref()).as_ref(), "test".as_bytes());
assert_eq!(BinaryBody::from_slice(b"test".as_ref()).len(), 4);
assert_eq!(BinaryBody::from_slice(b"test".as_ref()).as_ref(), "test".as_bytes());
assert_eq!(Binary::from(b"test".as_ref()).len(), 4);
assert_eq!(Binary::from(b"test".as_ref()).as_ref(), b"test");
assert_eq!(Binary::from_slice(b"test".as_ref()).len(), 4);
assert_eq!(Binary::from_slice(b"test".as_ref()).as_ref(), b"test");
}
#[test]
fn test_vec() {
assert_eq!(BinaryBody::from(Vec::from("test")).len(), 4);
assert_eq!(BinaryBody::from(Vec::from("test")).as_ref(), "test".as_bytes());
assert_eq!(Binary::from(Vec::from("test")).len(), 4);
assert_eq!(Binary::from(Vec::from("test")).as_ref(), b"test");
}
#[test]
fn test_bytes() {
assert_eq!(BinaryBody::from(Bytes::from("test")).len(), 4);
assert_eq!(BinaryBody::from(Bytes::from("test")).as_ref(), "test".as_bytes());
}
#[test]
fn test_rc_bytes() {
let b = Rc::new(Bytes::from("test"));
assert_eq!(BinaryBody::from(b.clone()).len(), 4);
assert_eq!(BinaryBody::from(b.clone()).as_ref(), "test".as_bytes());
assert_eq!(BinaryBody::from(&b).len(), 4);
assert_eq!(BinaryBody::from(&b).as_ref(), "test".as_bytes());
assert_eq!(Binary::from(Bytes::from("test")).len(), 4);
assert_eq!(Binary::from(Bytes::from("test")).as_ref(), b"test");
}
#[test]
fn test_ref_string() {
let b = Rc::new("test".to_owned());
assert_eq!(BinaryBody::from(&b).len(), 4);
assert_eq!(BinaryBody::from(&b).as_ref(), "test".as_bytes());
assert_eq!(Binary::from(&b).len(), 4);
assert_eq!(Binary::from(&b).as_ref(), b"test");
}
#[test]
fn test_rc_string() {
let b = Rc::new("test".to_owned());
assert_eq!(BinaryBody::from(b.clone()).len(), 4);
assert_eq!(BinaryBody::from(b.clone()).as_ref(), "test".as_bytes());
assert_eq!(BinaryBody::from(&b).len(), 4);
assert_eq!(BinaryBody::from(&b).as_ref(), "test".as_bytes());
}
#[test]
fn test_arc_bytes() {
let b = Arc::new(Bytes::from("test"));
assert_eq!(BinaryBody::from(b.clone()).len(), 4);
assert_eq!(BinaryBody::from(b.clone()).as_ref(), "test".as_bytes());
assert_eq!(BinaryBody::from(&b).len(), 4);
assert_eq!(BinaryBody::from(&b).as_ref(), "test".as_bytes());
assert_eq!(Binary::from(b.clone()).len(), 4);
assert_eq!(Binary::from(b.clone()).as_ref(), b"test");
assert_eq!(Binary::from(&b).len(), 4);
assert_eq!(Binary::from(&b).as_ref(), b"test");
}
#[test]
fn test_arc_string() {
let b = Arc::new("test".to_owned());
assert_eq!(BinaryBody::from(b.clone()).len(), 4);
assert_eq!(BinaryBody::from(b.clone()).as_ref(), "test".as_bytes());
assert_eq!(BinaryBody::from(&b).len(), 4);
assert_eq!(BinaryBody::from(&b).as_ref(), "test".as_bytes());
assert_eq!(Binary::from(b.clone()).len(), 4);
assert_eq!(Binary::from(b.clone()).as_ref(), b"test");
assert_eq!(Binary::from(&b).len(), 4);
assert_eq!(Binary::from(&b).as_ref(), b"test");
}
#[test]
fn test_string() {
let b = "test".to_owned();
assert_eq!(Binary::from(b.clone()).len(), 4);
assert_eq!(Binary::from(b.clone()).as_ref(), b"test");
assert_eq!(Binary::from(&b).len(), 4);
assert_eq!(Binary::from(&b).as_ref(), b"test");
}
#[test]
fn test_shared_vec() {
let b = Arc::new(Vec::from(&b"test"[..]));
assert_eq!(Binary::from(b.clone()).len(), 4);
assert_eq!(Binary::from(b.clone()).as_ref(), &b"test"[..]);
assert_eq!(Binary::from(&b).len(), 4);
assert_eq!(Binary::from(&b).as_ref(), &b"test"[..]);
}
#[test]
fn test_bytes_mut() {
let b = BytesMut::from("test");
assert_eq!(Binary::from(b.clone()).len(), 4);
assert_eq!(Binary::from(b).as_ref(), b"test");
}
#[test]
fn test_binary_into() {
let bytes = Bytes::from_static(b"test");
let b: Bytes = Binary::from("test").into();
assert_eq!(b, bytes);
let b: Bytes = Binary::from(bytes.clone()).into();
assert_eq!(b, bytes);
}
}

1237
src/client/connector.rs Normal file

File diff suppressed because it is too large Load Diff

121
src/client/mod.rs Normal file
View File

@ -0,0 +1,121 @@
//! Http client api
//!
//! ```rust
//! # extern crate actix;
//! # extern crate actix_web;
//! # extern crate futures;
//! # use futures::Future;
//! use actix_web::client;
//!
//! fn main() {
//! let sys = actix::System::new("test");
//!
//! actix::Arbiter::handle().spawn({
//! client::get("http://www.rust-lang.org") // <- Create request builder
//! .header("User-Agent", "Actix-web")
//! .finish().unwrap()
//! .send() // <- Send http request
//! .map_err(|_| ())
//! .and_then(|response| { // <- server http response
//! println!("Response: {:?}", response);
//! # actix::Arbiter::system().do_send(actix::msgs::SystemExit(0));
//! Ok(())
//! })
//! });
//!
//! sys.run();
//! }
//! ```
mod connector;
mod parser;
mod pipeline;
mod request;
mod response;
mod writer;
pub use self::connector::{
ClientConnector, ClientConnectorError, ClientConnectorStats, Connect, Connection,
Pause, Resume,
};
pub(crate) use self::parser::{HttpResponseParser, HttpResponseParserError};
pub use self::pipeline::{SendRequest, SendRequestError};
pub use self::request::{ClientRequest, ClientRequestBuilder};
pub use self::response::ClientResponse;
pub(crate) use self::writer::HttpClientWriter;
use error::ResponseError;
use http::Method;
use httpresponse::HttpResponse;
/// Convert `SendRequestError` to a `HttpResponse`
impl ResponseError for SendRequestError {
fn error_response(&self) -> HttpResponse {
match *self {
SendRequestError::Timeout => HttpResponse::GatewayTimeout(),
SendRequestError::Connector(_) => HttpResponse::BadGateway(),
_ => HttpResponse::InternalServerError(),
}.into()
}
}
/// Create request builder for `GET` requests
///
/// ```rust
/// # extern crate actix;
/// # extern crate actix_web;
/// # extern crate futures;
/// # use futures::Future;
/// use actix_web::client;
///
/// fn main() {
/// let sys = actix::System::new("test");
///
/// actix::Arbiter::handle().spawn({
/// client::get("http://www.rust-lang.org") // <- Create request builder
/// .header("User-Agent", "Actix-web")
/// .finish().unwrap()
/// .send() // <- Send http request
/// .map_err(|_| ())
/// .and_then(|response| { // <- server http response
/// println!("Response: {:?}", response);
/// # actix::Arbiter::system().do_send(actix::msgs::SystemExit(0));
/// Ok(())
/// })
/// });
///
/// sys.run();
/// }
/// ```
pub fn get<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::GET).uri(uri);
builder
}
/// Create request builder for `HEAD` requests
pub fn head<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::HEAD).uri(uri);
builder
}
/// Create request builder for `POST` requests
pub fn post<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::POST).uri(uri);
builder
}
/// Create request builder for `PUT` requests
pub fn put<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::PUT).uri(uri);
builder
}
/// Create request builder for `DELETE` requests
pub fn delete<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::DELETE).uri(uri);
builder
}

219
src/client/parser.rs Normal file
View File

@ -0,0 +1,219 @@
use bytes::{Bytes, BytesMut};
use futures::{Async, Poll};
use http::header::{self, HeaderName, HeaderValue};
use http::{HeaderMap, HttpTryFrom, StatusCode, Version};
use httparse;
use std::mem;
use error::{ParseError, PayloadError};
use server::h1decoder::EncodingDecoder;
use server::{utils, IoStream};
use super::response::ClientMessage;
use super::ClientResponse;
const MAX_BUFFER_SIZE: usize = 131_072;
const MAX_HEADERS: usize = 96;
#[derive(Default)]
pub struct HttpResponseParser {
decoder: Option<EncodingDecoder>,
}
#[derive(Debug, Fail)]
pub enum HttpResponseParserError {
/// Server disconnected
#[fail(display = "Server disconnected")]
Disconnect,
#[fail(display = "{}", _0)]
Error(#[cause] ParseError),
}
impl HttpResponseParser {
pub fn parse<T>(
&mut self, io: &mut T, buf: &mut BytesMut,
) -> Poll<ClientResponse, HttpResponseParserError>
where
T: IoStream,
{
// if buf is empty parse_message will always return NotReady, let's avoid that
if buf.is_empty() {
match utils::read_from_io(io, buf) {
Ok(Async::Ready(0)) => return Err(HttpResponseParserError::Disconnect),
Ok(Async::Ready(_)) => (),
Ok(Async::NotReady) => return Ok(Async::NotReady),
Err(err) => return Err(HttpResponseParserError::Error(err.into())),
}
}
loop {
match HttpResponseParser::parse_message(buf)
.map_err(HttpResponseParserError::Error)?
{
Async::Ready((msg, decoder)) => {
self.decoder = decoder;
return Ok(Async::Ready(msg));
}
Async::NotReady => {
if buf.capacity() >= MAX_BUFFER_SIZE {
return Err(HttpResponseParserError::Error(ParseError::TooLarge));
}
match utils::read_from_io(io, buf) {
Ok(Async::Ready(0)) => {
return Err(HttpResponseParserError::Disconnect)
}
Ok(Async::Ready(_)) => (),
Ok(Async::NotReady) => return Ok(Async::NotReady),
Err(err) => {
return Err(HttpResponseParserError::Error(err.into()))
}
}
}
}
}
}
pub fn parse_payload<T>(
&mut self, io: &mut T, buf: &mut BytesMut,
) -> Poll<Option<Bytes>, PayloadError>
where
T: IoStream,
{
if self.decoder.is_some() {
loop {
// read payload
let (not_ready, stream_finished) = match utils::read_from_io(io, buf) {
Ok(Async::Ready(0)) => (false, true),
Err(err) => return Err(err.into()),
Ok(Async::NotReady) => (true, false),
_ => (false, false),
};
match self.decoder.as_mut().unwrap().decode(buf) {
Ok(Async::Ready(Some(b))) => return Ok(Async::Ready(Some(b))),
Ok(Async::Ready(None)) => {
self.decoder.take();
return Ok(Async::Ready(None));
}
Ok(Async::NotReady) => {
if not_ready {
return Ok(Async::NotReady);
}
if stream_finished {
return Err(PayloadError::Incomplete);
}
}
Err(err) => return Err(err.into()),
}
}
} else {
Ok(Async::Ready(None))
}
}
fn parse_message(
buf: &mut BytesMut,
) -> Poll<(ClientResponse, Option<EncodingDecoder>), ParseError> {
// Parse http message
let bytes_ptr = buf.as_ref().as_ptr() as usize;
let mut headers: [httparse::Header; MAX_HEADERS] =
unsafe { mem::uninitialized() };
let (len, version, status, headers_len) = {
let b = unsafe {
let b: &[u8] = buf;
&*(b as *const _)
};
let mut resp = httparse::Response::new(&mut headers);
match resp.parse(b)? {
httparse::Status::Complete(len) => {
let version = if resp.version.unwrap_or(1) == 1 {
Version::HTTP_11
} else {
Version::HTTP_10
};
let status = StatusCode::from_u16(resp.code.unwrap())
.map_err(|_| ParseError::Status)?;
(len, version, status, resp.headers.len())
}
httparse::Status::Partial => return Ok(Async::NotReady),
}
};
let slice = buf.split_to(len).freeze();
// convert headers
let mut hdrs = HeaderMap::new();
for header in headers[..headers_len].iter() {
if let Ok(name) = HeaderName::try_from(header.name) {
let v_start = header.value.as_ptr() as usize - bytes_ptr;
let v_end = v_start + header.value.len();
let value = unsafe {
HeaderValue::from_shared_unchecked(slice.slice(v_start, v_end))
};
hdrs.append(name, value);
} else {
return Err(ParseError::Header);
}
}
let decoder = if status == StatusCode::SWITCHING_PROTOCOLS {
Some(EncodingDecoder::eof())
} else if let Some(len) = hdrs.get(header::CONTENT_LENGTH) {
// Content-Length
if let Ok(s) = len.to_str() {
if let Ok(len) = s.parse::<u64>() {
Some(EncodingDecoder::length(len))
} else {
debug!("illegal Content-Length: {:?}", len);
return Err(ParseError::Header);
}
} else {
debug!("illegal Content-Length: {:?}", len);
return Err(ParseError::Header);
}
} else if chunked(&hdrs)? {
// Chunked encoding
Some(EncodingDecoder::chunked())
} else {
None
};
if let Some(decoder) = decoder {
Ok(Async::Ready((
ClientResponse::new(ClientMessage {
status,
version,
headers: hdrs,
cookies: None,
}),
Some(decoder),
)))
} else {
Ok(Async::Ready((
ClientResponse::new(ClientMessage {
status,
version,
headers: hdrs,
cookies: None,
}),
None,
)))
}
}
}
/// Check if request has chunked transfer encoding
pub fn chunked(headers: &HeaderMap) -> Result<bool, ParseError> {
if let Some(encodings) = headers.get(header::TRANSFER_ENCODING) {
if let Ok(s) = encodings.to_str() {
Ok(s.to_lowercase().contains("chunked"))
} else {
Err(ParseError::Header)
}
} else {
Ok(false)
}
}

511
src/client/pipeline.rs Normal file
View File

@ -0,0 +1,511 @@
use bytes::{Bytes, BytesMut};
use futures::unsync::oneshot;
use futures::{Async, Future, Poll};
use http::header::CONTENT_ENCODING;
use std::time::Duration;
use std::{io, mem};
use tokio_core::reactor::Timeout;
use actix::prelude::*;
use super::HttpClientWriter;
use super::{ClientConnector, ClientConnectorError, Connect, Connection};
use super::{ClientRequest, ClientResponse};
use super::{HttpResponseParser, HttpResponseParserError};
use body::{Body, BodyStream};
use context::{ActorHttpContext, Frame};
use error::Error;
use error::PayloadError;
use header::ContentEncoding;
use httpmessage::HttpMessage;
use server::encoding::PayloadStream;
use server::shared::SharedBytes;
use server::WriterState;
/// A set of errors that can occur during request sending and response reading
#[derive(Fail, Debug)]
pub enum SendRequestError {
/// Response took too long
#[fail(display = "Timeout while waiting for response")]
Timeout,
/// Failed to connect to host
#[fail(display = "Failed to connect to host: {}", _0)]
Connector(#[cause] ClientConnectorError),
/// Error parsing response
#[fail(display = "{}", _0)]
ParseError(#[cause] HttpResponseParserError),
/// Error reading response payload
#[fail(display = "Error reading response payload: {}", _0)]
Io(#[cause] io::Error),
}
impl From<io::Error> for SendRequestError {
fn from(err: io::Error) -> SendRequestError {
SendRequestError::Io(err)
}
}
impl From<ClientConnectorError> for SendRequestError {
fn from(err: ClientConnectorError) -> SendRequestError {
match err {
ClientConnectorError::Timeout => SendRequestError::Timeout,
_ => SendRequestError::Connector(err),
}
}
}
enum State {
New,
Connect(actix::dev::Request<Unsync, ClientConnector, Connect>),
Connection(Connection),
Send(Box<Pipeline>),
None,
}
/// `SendRequest` is a `Future` which represents an asynchronous
/// request sending process.
#[must_use = "SendRequest does nothing unless polled"]
pub struct SendRequest {
req: ClientRequest,
state: State,
conn: Addr<Unsync, ClientConnector>,
conn_timeout: Duration,
wait_timeout: Duration,
timeout: Option<Timeout>,
}
impl SendRequest {
pub(crate) fn new(req: ClientRequest) -> SendRequest {
SendRequest::with_connector(req, ClientConnector::from_registry())
}
pub(crate) fn with_connector(
req: ClientRequest, conn: Addr<Unsync, ClientConnector>,
) -> SendRequest {
SendRequest {
req,
conn,
state: State::New,
timeout: None,
wait_timeout: Duration::from_secs(5),
conn_timeout: Duration::from_secs(1),
}
}
pub(crate) fn with_connection(req: ClientRequest, conn: Connection) -> SendRequest {
SendRequest {
req,
state: State::Connection(conn),
conn: ClientConnector::from_registry(),
timeout: None,
wait_timeout: Duration::from_secs(5),
conn_timeout: Duration::from_secs(1),
}
}
/// Set request timeout
///
/// Request timeout is the total time before a response must be received.
/// Default value is 5 seconds.
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(Timeout::new(timeout, Arbiter::handle()).unwrap());
self
}
/// Set connection timeout
///
/// Connection timeout includes resolving hostname and actual connection to
/// the host.
/// Default value is 1 second.
pub fn conn_timeout(mut self, timeout: Duration) -> Self {
self.conn_timeout = timeout;
self
}
/// Set wait timeout
///
/// If connections pool limits are enabled, wait time indicates max time
/// to wait for available connection. Default value is 5 seconds.
pub fn wait_timeout(mut self, timeout: Duration) -> Self {
self.wait_timeout = timeout;
self
}
}
impl Future for SendRequest {
type Item = ClientResponse;
type Error = SendRequestError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
loop {
let state = mem::replace(&mut self.state, State::None);
match state {
State::New => {
self.state = State::Connect(self.conn.send(Connect {
uri: self.req.uri().clone(),
wait_timeout: self.wait_timeout,
conn_timeout: self.conn_timeout,
}))
}
State::Connect(mut conn) => match conn.poll() {
Ok(Async::NotReady) => {
self.state = State::Connect(conn);
return Ok(Async::NotReady);
}
Ok(Async::Ready(result)) => match result {
Ok(stream) => self.state = State::Connection(stream),
Err(err) => return Err(err.into()),
},
Err(_) => {
return Err(SendRequestError::Connector(
ClientConnectorError::Disconnected,
))
}
},
State::Connection(conn) => {
let mut writer = HttpClientWriter::new(SharedBytes::default());
writer.start(&mut self.req)?;
let body = match self.req.replace_body(Body::Empty) {
Body::Streaming(stream) => IoBody::Payload(stream),
Body::Actor(ctx) => IoBody::Actor(ctx),
_ => IoBody::Done,
};
let timeout = self.timeout.take().unwrap_or_else(|| {
Timeout::new(Duration::from_secs(5), Arbiter::handle()).unwrap()
});
let pl = Box::new(Pipeline {
body,
writer,
conn: Some(conn),
parser: Some(HttpResponseParser::default()),
parser_buf: BytesMut::new(),
disconnected: false,
body_completed: false,
drain: None,
decompress: None,
should_decompress: self.req.response_decompress(),
write_state: RunningState::Running,
timeout: Some(timeout),
});
self.state = State::Send(pl);
}
State::Send(mut pl) => {
pl.poll_timeout()?;
pl.poll_write().map_err(|e| {
io::Error::new(io::ErrorKind::Other, format!("{}", e).as_str())
})?;
match pl.parse() {
Ok(Async::Ready(mut resp)) => {
resp.set_pipeline(pl);
return Ok(Async::Ready(resp));
}
Ok(Async::NotReady) => {
self.state = State::Send(pl);
return Ok(Async::NotReady);
}
Err(err) => return Err(SendRequestError::ParseError(err)),
}
}
State::None => unreachable!(),
}
}
}
}
pub(crate) struct Pipeline {
body: IoBody,
body_completed: bool,
conn: Option<Connection>,
writer: HttpClientWriter,
parser: Option<HttpResponseParser>,
parser_buf: BytesMut,
disconnected: bool,
drain: Option<oneshot::Sender<()>>,
decompress: Option<PayloadStream>,
should_decompress: bool,
write_state: RunningState,
timeout: Option<Timeout>,
}
enum IoBody {
Payload(BodyStream),
Actor(Box<ActorHttpContext>),
Done,
}
#[derive(Debug, PartialEq)]
enum RunningState {
Running,
Paused,
Done,
}
impl RunningState {
#[inline]
fn pause(&mut self) {
if *self != RunningState::Done {
*self = RunningState::Paused
}
}
#[inline]
fn resume(&mut self) {
if *self != RunningState::Done {
*self = RunningState::Running
}
}
}
impl Pipeline {
fn release_conn(&mut self) {
if let Some(conn) = self.conn.take() {
conn.release()
}
}
#[inline]
fn parse(&mut self) -> Poll<ClientResponse, HttpResponseParserError> {
if let Some(ref mut conn) = self.conn {
match self
.parser
.as_mut()
.unwrap()
.parse(conn, &mut self.parser_buf)
{
Ok(Async::Ready(resp)) => {
// check content-encoding
if self.should_decompress {
if let Some(enc) = resp.headers().get(CONTENT_ENCODING) {
if let Ok(enc) = enc.to_str() {
match ContentEncoding::from(enc) {
ContentEncoding::Auto
| ContentEncoding::Identity => (),
enc => {
self.decompress = Some(PayloadStream::new(enc))
}
}
}
}
}
Ok(Async::Ready(resp))
}
val => val,
}
} else {
Ok(Async::NotReady)
}
}
#[inline]
pub fn poll(&mut self) -> Poll<Option<Bytes>, PayloadError> {
if self.conn.is_none() {
return Ok(Async::Ready(None));
}
let conn: &mut Connection =
unsafe { &mut *(self.conn.as_mut().unwrap() as *mut _) };
let mut need_run = false;
// need write?
match self
.poll_write()
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{}", e)))?
{
Async::NotReady => need_run = true,
Async::Ready(_) => {
self.poll_timeout().map_err(|e| {
io::Error::new(io::ErrorKind::Other, format!("{}", e))
})?;
}
}
// need read?
if self.parser.is_some() {
loop {
match self
.parser
.as_mut()
.unwrap()
.parse_payload(conn, &mut self.parser_buf)?
{
Async::Ready(Some(b)) => {
if let Some(ref mut decompress) = self.decompress {
match decompress.feed_data(b) {
Ok(Some(b)) => return Ok(Async::Ready(Some(b))),
Ok(None) => return Ok(Async::NotReady),
Err(ref err)
if err.kind() == io::ErrorKind::WouldBlock =>
{
continue
}
Err(err) => return Err(err.into()),
}
} else {
return Ok(Async::Ready(Some(b)));
}
}
Async::Ready(None) => {
let _ = self.parser.take();
break;
}
Async::NotReady => return Ok(Async::NotReady),
}
}
}
// eof
if let Some(mut decompress) = self.decompress.take() {
let res = decompress.feed_eof();
if let Some(b) = res? {
self.release_conn();
return Ok(Async::Ready(Some(b)));
}
}
if need_run {
Ok(Async::NotReady)
} else {
self.release_conn();
Ok(Async::Ready(None))
}
}
fn poll_timeout(&mut self) -> Result<(), SendRequestError> {
if self.timeout.is_some() {
match self.timeout.as_mut().unwrap().poll() {
Ok(Async::Ready(())) => return Err(SendRequestError::Timeout),
Ok(Async::NotReady) => (),
Err(e) => return Err(e.into()),
}
}
Ok(())
}
#[inline]
fn poll_write(&mut self) -> Poll<(), Error> {
if self.write_state == RunningState::Done || self.conn.is_none() {
return Ok(Async::Ready(()));
}
let mut done = false;
if self.drain.is_none() && self.write_state != RunningState::Paused {
'outter: loop {
let result = match mem::replace(&mut self.body, IoBody::Done) {
IoBody::Payload(mut body) => match body.poll()? {
Async::Ready(None) => {
self.writer.write_eof()?;
self.body_completed = true;
break;
}
Async::Ready(Some(chunk)) => {
self.body = IoBody::Payload(body);
self.writer.write(chunk.into())?
}
Async::NotReady => {
done = true;
self.body = IoBody::Payload(body);
break;
}
},
IoBody::Actor(mut ctx) => {
if self.disconnected {
ctx.disconnected();
}
match ctx.poll()? {
Async::Ready(Some(vec)) => {
if vec.is_empty() {
self.body = IoBody::Actor(ctx);
break;
}
let mut res = None;
for frame in vec {
match frame {
Frame::Chunk(None) => {
self.body_completed = true;
self.writer.write_eof()?;
break 'outter;
}
Frame::Chunk(Some(chunk)) => {
res = Some(self.writer.write(chunk)?)
}
Frame::Drain(fut) => self.drain = Some(fut),
}
}
self.body = IoBody::Actor(ctx);
if self.drain.is_some() {
self.write_state.resume();
break;
}
res.unwrap()
}
Async::Ready(None) => {
done = true;
break;
}
Async::NotReady => {
done = true;
self.body = IoBody::Actor(ctx);
break;
}
}
}
IoBody::Done => {
self.body_completed = true;
done = true;
break;
}
};
match result {
WriterState::Pause => {
self.write_state.pause();
break;
}
WriterState::Done => self.write_state.resume(),
}
}
}
// flush io but only if we need to
match self
.writer
.poll_completed(self.conn.as_mut().unwrap(), false)
{
Ok(Async::Ready(_)) => {
if self.disconnected
|| (self.body_completed && self.writer.is_completed())
{
self.write_state = RunningState::Done;
} else {
self.write_state.resume();
}
// resolve drain futures
if let Some(tx) = self.drain.take() {
let _ = tx.send(());
}
// restart io processing
if !done || self.write_state == RunningState::Done {
self.poll_write()
} else {
Ok(Async::NotReady)
}
}
Ok(Async::NotReady) => Ok(Async::NotReady),
Err(err) => Err(err.into()),
}
}
}
impl Drop for Pipeline {
fn drop(&mut self) {
if let Some(conn) = self.conn.take() {
conn.close()
}
}
}

718
src/client/request.rs Normal file
View File

@ -0,0 +1,718 @@
use std::fmt::Write as FmtWrite;
use std::io::Write;
use std::time::Duration;
use std::{fmt, mem};
use actix::{Addr, Unsync};
use bytes::{BufMut, Bytes, BytesMut};
use cookie::{Cookie, CookieJar};
use futures::Stream;
use percent_encoding::{percent_encode, USERINFO_ENCODE_SET};
use serde::Serialize;
use serde_json;
use url::Url;
use super::connector::{ClientConnector, Connection};
use super::pipeline::SendRequest;
use body::Body;
use error::Error;
use header::{ContentEncoding, Header, IntoHeaderValue};
use http::header::{self, HeaderName, HeaderValue};
use http::{uri, Error as HttpError, HeaderMap, HttpTryFrom, Method, Uri, Version};
use httpmessage::HttpMessage;
use httprequest::HttpRequest;
/// An HTTP Client Request
///
/// ```rust
/// # extern crate actix;
/// # extern crate actix_web;
/// # extern crate futures;
/// # use futures::Future;
/// use actix_web::client::ClientRequest;
///
/// fn main() {
/// let sys = actix::System::new("test");
///
/// actix::Arbiter::handle().spawn({
/// ClientRequest::get("http://www.rust-lang.org") // <- Create request builder
/// .header("User-Agent", "Actix-web")
/// .finish().unwrap()
/// .send() // <- Send http request
/// .map_err(|_| ())
/// .and_then(|response| { // <- server http response
/// println!("Response: {:?}", response);
/// # actix::Arbiter::system().do_send(actix::msgs::SystemExit(0));
/// Ok(())
/// })
/// });
///
/// sys.run();
/// }
/// ```
pub struct ClientRequest {
uri: Uri,
method: Method,
version: Version,
headers: HeaderMap,
body: Body,
chunked: bool,
upgrade: bool,
timeout: Option<Duration>,
encoding: ContentEncoding,
response_decompress: bool,
buffer_capacity: usize,
conn: ConnectionType,
}
enum ConnectionType {
Default,
Connector(Addr<Unsync, ClientConnector>),
Connection(Connection),
}
impl Default for ClientRequest {
fn default() -> ClientRequest {
ClientRequest {
uri: Uri::default(),
method: Method::default(),
version: Version::HTTP_11,
headers: HeaderMap::with_capacity(16),
body: Body::Empty,
chunked: false,
upgrade: false,
timeout: None,
encoding: ContentEncoding::Auto,
response_decompress: true,
buffer_capacity: 32_768,
conn: ConnectionType::Default,
}
}
}
impl ClientRequest {
/// Create request builder for `GET` request
pub fn get<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::GET).uri(uri);
builder
}
/// Create request builder for `HEAD` request
pub fn head<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::HEAD).uri(uri);
builder
}
/// Create request builder for `POST` request
pub fn post<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::POST).uri(uri);
builder
}
/// Create request builder for `PUT` request
pub fn put<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::PUT).uri(uri);
builder
}
/// Create request builder for `DELETE` request
pub fn delete<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::DELETE).uri(uri);
builder
}
}
impl ClientRequest {
/// Create client request builder
pub fn build() -> ClientRequestBuilder {
ClientRequestBuilder {
request: Some(ClientRequest::default()),
err: None,
cookies: None,
default_headers: true,
}
}
/// Create client request builder
pub fn build_from<T: Into<ClientRequestBuilder>>(source: T) -> ClientRequestBuilder {
source.into()
}
/// Get the request URI
#[inline]
pub fn uri(&self) -> &Uri {
&self.uri
}
/// Set client request URI
#[inline]
pub fn set_uri(&mut self, uri: Uri) {
self.uri = uri
}
/// Get the request method
#[inline]
pub fn method(&self) -> &Method {
&self.method
}
/// Set HTTP `Method` for the request
#[inline]
pub fn set_method(&mut self, method: Method) {
self.method = method
}
/// Get HTTP version for the request
#[inline]
pub fn version(&self) -> Version {
self.version
}
/// Set http `Version` for the request
#[inline]
pub fn set_version(&mut self, version: Version) {
self.version = version
}
/// Get the headers from the request
#[inline]
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
/// Get a mutable reference to the headers
#[inline]
pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers
}
/// is chunked encoding enabled
#[inline]
pub fn chunked(&self) -> bool {
self.chunked
}
/// is upgrade request
#[inline]
pub fn upgrade(&self) -> bool {
self.upgrade
}
/// Content encoding
#[inline]
pub fn content_encoding(&self) -> ContentEncoding {
self.encoding
}
/// Decompress response payload
#[inline]
pub fn response_decompress(&self) -> bool {
self.response_decompress
}
/// Requested write buffer capacity
pub fn write_buffer_capacity(&self) -> usize {
self.buffer_capacity
}
/// Get body of this response
#[inline]
pub fn body(&self) -> &Body {
&self.body
}
/// Set a body
pub fn set_body<B: Into<Body>>(&mut self, body: B) {
self.body = body.into();
}
/// Extract body, replace it with `Empty`
pub(crate) fn replace_body(&mut self, body: Body) -> Body {
mem::replace(&mut self.body, body)
}
/// Send request
///
/// This method returns a future that resolves to a ClientResponse
pub fn send(mut self) -> SendRequest {
let timeout = self.timeout.take();
let send = match mem::replace(&mut self.conn, ConnectionType::Default) {
ConnectionType::Default => SendRequest::new(self),
ConnectionType::Connector(conn) => SendRequest::with_connector(self, conn),
ConnectionType::Connection(conn) => SendRequest::with_connection(self, conn),
};
if let Some(timeout) = timeout {
send.timeout(timeout)
} else {
send
}
}
}
impl fmt::Debug for ClientRequest {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let res = writeln!(
f,
"\nClientRequest {:?} {}:{}",
self.version, self.method, self.uri
);
let _ = writeln!(f, " headers:");
for (key, val) in self.headers.iter() {
let _ = writeln!(f, " {:?}: {:?}", key, val);
}
res
}
}
/// An HTTP Client request builder
///
/// This type can be used to construct an instance of `ClientRequest` through a
/// builder-like pattern.
pub struct ClientRequestBuilder {
request: Option<ClientRequest>,
err: Option<HttpError>,
cookies: Option<CookieJar>,
default_headers: bool,
}
impl ClientRequestBuilder {
/// Set HTTP URI of request.
#[inline]
pub fn uri<U: AsRef<str>>(&mut self, uri: U) -> &mut Self {
match Url::parse(uri.as_ref()) {
Ok(url) => self._uri(url.as_str()),
Err(_) => self._uri(uri.as_ref()),
}
}
fn _uri(&mut self, url: &str) -> &mut Self {
match Uri::try_from(url) {
Ok(uri) => {
// set request host header
if let Some(host) = uri.host() {
self.set_header(header::HOST, host);
}
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.uri = uri;
}
}
Err(e) => self.err = Some(e.into()),
}
self
}
/// Set HTTP method of this request.
#[inline]
pub fn method(&mut self, method: Method) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.method = method;
}
self
}
/// Set HTTP method of this request.
#[inline]
pub fn get_method(&mut self) -> &Method {
let parts =
parts(&mut self.request, &self.err).expect("cannot reuse request builder");
&parts.method
}
/// Set HTTP version of this request.
///
/// By default requests's HTTP version depends on network stream
#[inline]
pub fn version(&mut self, version: Version) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.version = version;
}
self
}
/// Set a header.
///
/// ```rust
/// # extern crate mime;
/// # extern crate actix_web;
/// # use actix_web::client::*;
/// #
/// use actix_web::{client, http};
///
/// fn main() {
/// let req = client::ClientRequest::build()
/// .set(http::header::Date::now())
/// .set(http::header::ContentType(mime::TEXT_HTML))
/// .finish().unwrap();
/// }
/// ```
#[doc(hidden)]
pub fn set<H: Header>(&mut self, hdr: H) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
match hdr.try_into() {
Ok(value) => {
parts.headers.insert(H::name(), value);
}
Err(e) => self.err = Some(e.into()),
}
}
self
}
/// Append a header.
///
/// Header gets appended to existing header.
/// To override header use `set_header()` method.
///
/// ```rust
/// # extern crate http;
/// # extern crate actix_web;
/// # use actix_web::client::*;
/// #
/// use http::header;
///
/// fn main() {
/// let req = ClientRequest::build()
/// .header("X-TEST", "value")
/// .header(header::CONTENT_TYPE, "application/json")
/// .finish().unwrap();
/// }
/// ```
pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
where
HeaderName: HttpTryFrom<K>,
V: IntoHeaderValue,
{
if let Some(parts) = parts(&mut self.request, &self.err) {
match HeaderName::try_from(key) {
Ok(key) => match value.try_into() {
Ok(value) => {
parts.headers.append(key, value);
}
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Set a header.
pub fn set_header<K, V>(&mut self, key: K, value: V) -> &mut Self
where
HeaderName: HttpTryFrom<K>,
V: IntoHeaderValue,
{
if let Some(parts) = parts(&mut self.request, &self.err) {
match HeaderName::try_from(key) {
Ok(key) => match value.try_into() {
Ok(value) => {
parts.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Set content encoding.
///
/// By default `ContentEncoding::Identity` is used.
#[inline]
pub fn content_encoding(&mut self, enc: ContentEncoding) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.encoding = enc;
}
self
}
/// Enables automatic chunked transfer encoding
#[inline]
pub fn chunked(&mut self) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.chunked = true;
}
self
}
/// Enable connection upgrade
#[inline]
pub fn upgrade(&mut self) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.upgrade = true;
}
self
}
/// Set request's content type
#[inline]
pub fn content_type<V>(&mut self, value: V) -> &mut Self
where
HeaderValue: HttpTryFrom<V>,
{
if let Some(parts) = parts(&mut self.request, &self.err) {
match HeaderValue::try_from(value) {
Ok(value) => {
parts.headers.insert(header::CONTENT_TYPE, value);
}
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Set content length
#[inline]
pub fn content_length(&mut self, len: u64) -> &mut Self {
let mut wrt = BytesMut::new().writer();
let _ = write!(wrt, "{}", len);
self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze())
}
/// Set a cookie
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::{client, http};
///
/// fn main() {
/// let req = client::ClientRequest::build()
/// .cookie(
/// http::Cookie::build("name", "value")
/// .domain("www.rust-lang.org")
/// .path("/")
/// .secure(true)
/// .http_only(true)
/// .finish())
/// .finish().unwrap();
/// }
/// ```
pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self {
if self.cookies.is_none() {
let mut jar = CookieJar::new();
jar.add(cookie.into_owned());
self.cookies = Some(jar)
} else {
self.cookies.as_mut().unwrap().add(cookie.into_owned());
}
self
}
/// Do not add default request headers.
/// By default `Accept-Encoding` header is set.
pub fn no_default_headers(&mut self) -> &mut Self {
self.default_headers = false;
self
}
/// Disable automatic decompress response body
pub fn disable_decompress(&mut self) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.response_decompress = false;
}
self
}
/// Set write buffer capacity
///
/// Default buffer capacity is 32kb
pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.buffer_capacity = cap;
}
self
}
/// Set request timeout
///
/// Request timeout is a total time before response should be received.
/// Default value is 5 seconds.
pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.timeout = Some(timeout);
}
self
}
/// Send request using custom connector
pub fn with_connector(&mut self, conn: Addr<Unsync, ClientConnector>) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.conn = ConnectionType::Connector(conn);
}
self
}
/// Send request using existing `Connection`
pub fn with_connection(&mut self, conn: Connection) -> &mut Self {
if let Some(parts) = parts(&mut self.request, &self.err) {
parts.conn = ConnectionType::Connection(conn);
}
self
}
/// This method calls provided closure with builder reference if
/// value is `true`.
pub fn if_true<F>(&mut self, value: bool, f: F) -> &mut Self
where
F: FnOnce(&mut ClientRequestBuilder),
{
if value {
f(self);
}
self
}
/// This method calls provided closure with builder reference if
/// value is `Some`.
pub fn if_some<T, F>(&mut self, value: Option<T>, f: F) -> &mut Self
where
F: FnOnce(T, &mut ClientRequestBuilder),
{
if let Some(val) = value {
f(val, self);
}
self
}
/// Set a body and generate `ClientRequest`.
///
/// `ClientRequestBuilder` can not be used after this call.
pub fn body<B: Into<Body>>(&mut self, body: B) -> Result<ClientRequest, Error> {
if let Some(e) = self.err.take() {
return Err(e.into());
}
if self.default_headers {
// enable br only for https
let https = if let Some(parts) = parts(&mut self.request, &self.err) {
parts
.uri
.scheme_part()
.map(|s| s == &uri::Scheme::HTTPS)
.unwrap_or(true)
} else {
true
};
if https {
self.header(header::ACCEPT_ENCODING, "br, gzip, deflate");
} else {
self.header(header::ACCEPT_ENCODING, "gzip, deflate");
}
}
let mut request = self.request.take().expect("cannot reuse request builder");
// set cookies
if let Some(ref mut jar) = self.cookies {
let mut cookie = String::new();
for c in jar.delta() {
let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET);
let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET);
let _ = write!(&mut cookie, "; {}={}", name, value);
}
request.headers.insert(
header::COOKIE,
HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(),
);
}
request.body = body.into();
Ok(request)
}
/// Set a JSON body and generate `ClientRequest`
///
/// `ClientRequestBuilder` can not be used after this call.
pub fn json<T: Serialize>(&mut self, value: T) -> Result<ClientRequest, Error> {
let body = serde_json::to_string(&value)?;
let contains = if let Some(parts) = parts(&mut self.request, &self.err) {
parts.headers.contains_key(header::CONTENT_TYPE)
} else {
true
};
if !contains {
self.header(header::CONTENT_TYPE, "application/json");
}
self.body(body)
}
/// Set a streaming body and generate `ClientRequest`.
///
/// `ClientRequestBuilder` can not be used after this call.
pub fn streaming<S, E>(&mut self, stream: S) -> Result<ClientRequest, Error>
where
S: Stream<Item = Bytes, Error = E> + 'static,
E: Into<Error>,
{
self.body(Body::Streaming(Box::new(stream.map_err(|e| e.into()))))
}
/// Set an empty body and generate `ClientRequest`
///
/// `ClientRequestBuilder` can not be used after this call.
pub fn finish(&mut self) -> Result<ClientRequest, Error> {
self.body(Body::Empty)
}
/// This method construct new `ClientRequestBuilder`
pub fn take(&mut self) -> ClientRequestBuilder {
ClientRequestBuilder {
request: self.request.take(),
err: self.err.take(),
cookies: self.cookies.take(),
default_headers: self.default_headers,
}
}
}
#[inline]
fn parts<'a>(
parts: &'a mut Option<ClientRequest>, err: &Option<HttpError>,
) -> Option<&'a mut ClientRequest> {
if err.is_some() {
return None;
}
parts.as_mut()
}
impl fmt::Debug for ClientRequestBuilder {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(ref parts) = self.request {
let res = writeln!(
f,
"\nClientRequestBuilder {:?} {}:{}",
parts.version, parts.method, parts.uri
);
let _ = writeln!(f, " headers:");
for (key, val) in parts.headers.iter() {
let _ = writeln!(f, " {:?}: {:?}", key, val);
}
res
} else {
write!(f, "ClientRequestBuilder(Consumed)")
}
}
}
/// Create `ClientRequestBuilder` from `HttpRequest`
///
/// It is useful for proxy requests. This implementation
/// copies all request headers and the method.
impl<'a, S: 'static> From<&'a HttpRequest<S>> for ClientRequestBuilder {
fn from(req: &'a HttpRequest<S>) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
for (key, value) in req.headers() {
builder.header(key.clone(), value.clone());
}
builder.method(req.method().clone());
builder
}
}

146
src/client/response.rs Normal file
View File

@ -0,0 +1,146 @@
use std::cell::UnsafeCell;
use std::rc::Rc;
use std::{fmt, str};
use bytes::Bytes;
use cookie::Cookie;
use futures::{Async, Poll, Stream};
use http::header::{self, HeaderValue};
use http::{HeaderMap, StatusCode, Version};
use error::{CookieParseError, PayloadError};
use httpmessage::HttpMessage;
use super::pipeline::Pipeline;
pub(crate) struct ClientMessage {
pub status: StatusCode,
pub version: Version,
pub headers: HeaderMap<HeaderValue>,
pub cookies: Option<Vec<Cookie<'static>>>,
}
impl Default for ClientMessage {
fn default() -> ClientMessage {
ClientMessage {
status: StatusCode::OK,
version: Version::HTTP_11,
headers: HeaderMap::with_capacity(16),
cookies: None,
}
}
}
/// An HTTP Client response
pub struct ClientResponse(Rc<UnsafeCell<ClientMessage>>, Option<Box<Pipeline>>);
impl HttpMessage for ClientResponse {
/// Get the headers from the response.
#[inline]
fn headers(&self) -> &HeaderMap {
&self.as_ref().headers
}
}
impl ClientResponse {
pub(crate) fn new(msg: ClientMessage) -> ClientResponse {
ClientResponse(Rc::new(UnsafeCell::new(msg)), None)
}
pub(crate) fn set_pipeline(&mut self, pl: Box<Pipeline>) {
self.1 = Some(pl);
}
#[inline]
fn as_ref(&self) -> &ClientMessage {
unsafe { &*self.0.get() }
}
#[inline]
#[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))]
fn as_mut(&self) -> &mut ClientMessage {
unsafe { &mut *self.0.get() }
}
/// Get the HTTP version of this response.
#[inline]
pub fn version(&self) -> Version {
self.as_ref().version
}
/// Get the status from the server.
#[inline]
pub fn status(&self) -> StatusCode {
self.as_ref().status
}
/// Load response cookies.
pub fn cookies(&self) -> Result<&Vec<Cookie<'static>>, CookieParseError> {
if self.as_ref().cookies.is_none() {
let msg = self.as_mut();
let mut cookies = Vec::new();
for val in msg.headers.get_all(header::SET_COOKIE).iter() {
let s = str::from_utf8(val.as_bytes()).map_err(CookieParseError::from)?;
cookies.push(Cookie::parse_encoded(s)?.into_owned());
}
msg.cookies = Some(cookies)
}
Ok(self.as_ref().cookies.as_ref().unwrap())
}
/// Return request cookie.
pub fn cookie(&self, name: &str) -> Option<&Cookie> {
if let Ok(cookies) = self.cookies() {
for cookie in cookies {
if cookie.name() == name {
return Some(cookie);
}
}
}
None
}
}
impl fmt::Debug for ClientResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let res = writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status());
let _ = writeln!(f, " headers:");
for (key, val) in self.headers().iter() {
let _ = writeln!(f, " {:?}: {:?}", key, val);
}
res
}
}
/// Future that resolves to a complete request body.
impl Stream for ClientResponse {
type Item = Bytes;
type Error = PayloadError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
if let Some(ref mut pl) = self.1 {
pl.poll()
} else {
Ok(Async::Ready(None))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debug() {
let resp = ClientResponse::new(ClientMessage::default());
resp.as_mut()
.headers
.insert(header::COOKIE, HeaderValue::from_static("cookie1=value1"));
resp.as_mut()
.headers
.insert(header::COOKIE, HeaderValue::from_static("cookie2=value2"));
let dbg = format!("{:?}", resp);
assert!(dbg.contains("ClientResponse"));
}
}

396
src/client/writer.rs Normal file
View File

@ -0,0 +1,396 @@
#![cfg_attr(feature = "cargo-clippy", allow(redundant_field_names))]
use std::cell::RefCell;
use std::fmt::Write as FmtWrite;
use std::io::{self, Write};
#[cfg(feature = "brotli")]
use brotli2::write::BrotliEncoder;
use bytes::{BufMut, BytesMut};
#[cfg(feature = "flate2")]
use flate2::write::{DeflateEncoder, GzEncoder};
#[cfg(feature = "flate2")]
use flate2::Compression;
use futures::{Async, Poll};
use http::header::{
HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING,
};
use http::{HttpTryFrom, Version};
use time::{self, Duration};
use tokio_io::AsyncWrite;
use body::{Binary, Body};
use header::ContentEncoding;
use server::encoding::{ContentEncoder, TransferEncoding};
use server::shared::SharedBytes;
use server::WriterState;
use client::ClientRequest;
const AVERAGE_HEADER_SIZE: usize = 30;
bitflags! {
struct Flags: u8 {
const STARTED = 0b0000_0001;
const UPGRADE = 0b0000_0010;
const KEEPALIVE = 0b0000_0100;
const DISCONNECTED = 0b0000_1000;
}
}
pub(crate) struct HttpClientWriter {
flags: Flags,
written: u64,
headers_size: u32,
buffer: SharedBytes,
buffer_capacity: usize,
encoder: ContentEncoder,
}
impl HttpClientWriter {
pub fn new(buffer: SharedBytes) -> HttpClientWriter {
let encoder = ContentEncoder::Identity(TransferEncoding::eof(buffer.clone()));
HttpClientWriter {
flags: Flags::empty(),
written: 0,
headers_size: 0,
buffer_capacity: 0,
buffer,
encoder,
}
}
pub fn disconnected(&mut self) {
self.buffer.take();
}
pub fn is_completed(&self) -> bool {
self.buffer.is_empty()
}
// pub fn keepalive(&self) -> bool {
// self.flags.contains(Flags::KEEPALIVE) &&
// !self.flags.contains(Flags::UPGRADE) }
fn write_to_stream<T: AsyncWrite>(
&mut self, stream: &mut T,
) -> io::Result<WriterState> {
while !self.buffer.is_empty() {
match stream.write(self.buffer.as_ref()) {
Ok(0) => {
self.disconnected();
return Ok(WriterState::Done);
}
Ok(n) => {
let _ = self.buffer.split_to(n);
}
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
if self.buffer.len() > self.buffer_capacity {
return Ok(WriterState::Pause);
} else {
return Ok(WriterState::Done);
}
}
Err(err) => return Err(err),
}
}
Ok(WriterState::Done)
}
}
impl HttpClientWriter {
pub fn start(&mut self, msg: &mut ClientRequest) -> io::Result<()> {
// prepare task
self.flags.insert(Flags::STARTED);
self.encoder = content_encoder(self.buffer.clone(), msg);
if msg.upgrade() {
self.flags.insert(Flags::UPGRADE);
}
// render message
{
// status line
writeln!(
self.buffer,
"{} {} {:?}\r",
msg.method(),
msg.uri()
.path_and_query()
.map(|u| u.as_str())
.unwrap_or("/"),
msg.version()
)?;
// write headers
let mut buffer = self.buffer.get_mut();
if let Body::Binary(ref bytes) = *msg.body() {
buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE + bytes.len());
} else {
buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE);
}
for (key, value) in msg.headers() {
let v = value.as_ref();
let k = key.as_str().as_bytes();
buffer.reserve(k.len() + v.len() + 4);
buffer.put_slice(k);
buffer.put_slice(b": ");
buffer.put_slice(v);
buffer.put_slice(b"\r\n");
}
// set date header
if !msg.headers().contains_key(DATE) {
buffer.extend_from_slice(b"date: ");
set_date(&mut buffer);
buffer.extend_from_slice(b"\r\n\r\n");
} else {
buffer.extend_from_slice(b"\r\n");
}
self.headers_size = buffer.len() as u32;
if msg.body().is_binary() {
if let Body::Binary(bytes) = msg.replace_body(Body::Empty) {
self.written += bytes.len() as u64;
self.encoder.write(bytes)?;
}
} else {
self.buffer_capacity = msg.write_buffer_capacity();
}
}
Ok(())
}
pub fn write(&mut self, payload: Binary) -> io::Result<WriterState> {
self.written += payload.len() as u64;
if !self.flags.contains(Flags::DISCONNECTED) {
if self.flags.contains(Flags::UPGRADE) {
self.buffer.extend(payload);
} else {
self.encoder.write(payload)?;
}
}
if self.buffer.len() > self.buffer_capacity {
Ok(WriterState::Pause)
} else {
Ok(WriterState::Done)
}
}
pub fn write_eof(&mut self) -> io::Result<()> {
self.encoder.write_eof()?;
if self.encoder.is_eof() {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::Other,
"Last payload item, but eof is not reached",
))
}
}
#[inline]
pub fn poll_completed<T: AsyncWrite>(
&mut self, stream: &mut T, shutdown: bool,
) -> Poll<(), io::Error> {
match self.write_to_stream(stream) {
Ok(WriterState::Done) => {
if shutdown {
stream.shutdown()
} else {
Ok(Async::Ready(()))
}
}
Ok(WriterState::Pause) => Ok(Async::NotReady),
Err(err) => Err(err),
}
}
}
fn content_encoder(buf: SharedBytes, req: &mut ClientRequest) -> ContentEncoder {
let version = req.version();
let mut body = req.replace_body(Body::Empty);
let mut encoding = req.content_encoding();
let transfer = match body {
Body::Empty => {
req.headers_mut().remove(CONTENT_LENGTH);
TransferEncoding::length(0, buf)
}
Body::Binary(ref mut bytes) => {
if encoding.is_compression() {
let tmp = SharedBytes::default();
let transfer = TransferEncoding::eof(tmp.clone());
let mut enc = match encoding {
#[cfg(feature = "flate2")]
ContentEncoding::Deflate => ContentEncoder::Deflate(
DeflateEncoder::new(transfer, Compression::default()),
),
#[cfg(feature = "flate2")]
ContentEncoding::Gzip => ContentEncoder::Gzip(GzEncoder::new(
transfer,
Compression::default(),
)),
#[cfg(feature = "brotli")]
ContentEncoding::Br => {
ContentEncoder::Br(BrotliEncoder::new(transfer, 5))
}
ContentEncoding::Identity => ContentEncoder::Identity(transfer),
ContentEncoding::Auto => unreachable!(),
};
// TODO return error!
let _ = enc.write(bytes.clone());
let _ = enc.write_eof();
*bytes = Binary::from(tmp.take());
req.headers_mut().insert(
CONTENT_ENCODING,
HeaderValue::from_static(encoding.as_str()),
);
encoding = ContentEncoding::Identity;
}
let mut b = BytesMut::new();
let _ = write!(b, "{}", bytes.len());
req.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap());
TransferEncoding::eof(buf)
}
Body::Streaming(_) | Body::Actor(_) => {
if req.upgrade() {
if version == Version::HTTP_2 {
error!("Connection upgrade is forbidden for HTTP/2");
} else {
req.headers_mut()
.insert(CONNECTION, HeaderValue::from_static("upgrade"));
}
if encoding != ContentEncoding::Identity {
encoding = ContentEncoding::Identity;
req.headers_mut().remove(CONTENT_ENCODING);
}
TransferEncoding::eof(buf)
} else {
streaming_encoding(buf, version, req)
}
}
};
if encoding.is_compression() {
req.headers_mut().insert(
CONTENT_ENCODING,
HeaderValue::from_static(encoding.as_str()),
);
}
req.replace_body(body);
match encoding {
#[cfg(feature = "flate2")]
ContentEncoding::Deflate => ContentEncoder::Deflate(DeflateEncoder::new(
transfer,
Compression::default(),
)),
#[cfg(feature = "flate2")]
ContentEncoding::Gzip => {
ContentEncoder::Gzip(GzEncoder::new(transfer, Compression::default()))
}
#[cfg(feature = "brotli")]
ContentEncoding::Br => ContentEncoder::Br(BrotliEncoder::new(transfer, 5)),
ContentEncoding::Identity | ContentEncoding::Auto => {
ContentEncoder::Identity(transfer)
}
}
}
fn streaming_encoding(
buf: SharedBytes, version: Version, req: &mut ClientRequest,
) -> TransferEncoding {
if req.chunked() {
// Enable transfer encoding
req.headers_mut().remove(CONTENT_LENGTH);
if version == Version::HTTP_2 {
req.headers_mut().remove(TRANSFER_ENCODING);
TransferEncoding::eof(buf)
} else {
req.headers_mut()
.insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
TransferEncoding::chunked(buf)
}
} else {
// if Content-Length is specified, then use it as length hint
let (len, chunked) = if let Some(len) = req.headers().get(CONTENT_LENGTH) {
// Content-Length
if let Ok(s) = len.to_str() {
if let Ok(len) = s.parse::<u64>() {
(Some(len), false)
} else {
error!("illegal Content-Length: {:?}", len);
(None, false)
}
} else {
error!("illegal Content-Length: {:?}", len);
(None, false)
}
} else {
(None, true)
};
if !chunked {
if let Some(len) = len {
TransferEncoding::length(len, buf)
} else {
TransferEncoding::eof(buf)
}
} else {
// Enable transfer encoding
match version {
Version::HTTP_11 => {
req.headers_mut()
.insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
TransferEncoding::chunked(buf)
}
_ => {
req.headers_mut().remove(TRANSFER_ENCODING);
TransferEncoding::eof(buf)
}
}
}
}
}
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
pub const DATE_VALUE_LENGTH: usize = 29;
fn set_date(dst: &mut BytesMut) {
CACHED.with(|cache| {
let mut cache = cache.borrow_mut();
let now = time::get_time();
if now > cache.next_update {
cache.update(now);
}
dst.extend_from_slice(cache.buffer());
})
}
struct CachedDate {
bytes: [u8; DATE_VALUE_LENGTH],
next_update: time::Timespec,
}
thread_local!(static CACHED: RefCell<CachedDate> = RefCell::new(CachedDate {
bytes: [0; DATE_VALUE_LENGTH],
next_update: time::Timespec::new(0, 0),
}));
impl CachedDate {
fn buffer(&self) -> &[u8] {
&self.bytes[..]
}
fn update(&mut self, now: time::Timespec) {
write!(&mut self.bytes[..], "{}", time::at_utc(now).rfc822()).unwrap();
self.next_update = now + Duration::seconds(1);
self.next_update.nsec = 0;
}
}

View File

@ -1,309 +1,272 @@
use std;
use std::rc::Rc;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::marker::PhantomData;
use futures::{Async, Future, Stream, Poll};
use futures::sync::oneshot::Sender;
use futures::unsync::oneshot;
use futures::{Async, Future, Poll};
use smallvec::SmallVec;
use std::marker::PhantomData;
use actix::{Actor, ActorState, ActorContext, AsyncContext,
Handler, Subscriber, ResponseType};
use actix::dev::{ContextImpl, SyncEnvelope, ToEnvelope};
use actix::fut::ActorFuture;
use actix::dev::{AsyncContextApi, ActorAddressCell, ActorItemsCell, ActorWaitCell, SpawnHandle,
Envelope, ToEnvelope, RemoteEnvelope};
use actix::{
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle,
Syn, Unsync,
};
use task::{IoContext, DrainFut};
use body::BinaryBody;
use route::{Route, Frame};
use httpresponse::HttpResponse;
use body::{Binary, Body};
use error::{Error, ErrorInternalServerError};
use httprequest::HttpRequest;
pub trait ActorHttpContext: 'static {
fn disconnected(&mut self);
fn poll(&mut self) -> Poll<Option<SmallVec<[Frame; 4]>>, Error>;
}
/// Actor execution context
pub struct HttpContext<A> where A: Actor<Context=HttpContext<A>> + Route,
#[derive(Debug)]
pub enum Frame {
Chunk(Option<Binary>),
Drain(oneshot::Sender<()>),
}
impl Frame {
pub fn len(&self) -> usize {
match *self {
Frame::Chunk(Some(ref bin)) => bin.len(),
_ => 0,
}
}
}
/// Execution context for http actors
pub struct HttpContext<A, S = ()>
where
A: Actor<Context = HttpContext<A, S>>,
{
act: Option<A>,
state: ActorState,
modified: bool,
items: ActorItemsCell<A>,
address: ActorAddressCell<A>,
stream: VecDeque<Frame>,
wait: ActorWaitCell<A>,
app_state: Rc<<A as Route>::State>,
inner: ContextImpl<A>,
stream: Option<SmallVec<[Frame; 4]>>,
request: HttpRequest<S>,
disconnected: bool,
}
impl<A> IoContext for HttpContext<A> where A: Actor<Context=Self> + Route {
fn disconnected(&mut self) {
self.items.stop();
self.disconnected = true;
if self.state == ActorState::Running {
self.state = ActorState::Stopping;
}
}
}
impl<A> ActorContext for HttpContext<A> where A: Actor<Context=Self> + Route
impl<A, S> ActorContext for HttpContext<A, S>
where
A: Actor<Context = Self>,
{
/// Stop actor execution
fn stop(&mut self) {
self.items.stop();
self.address.close();
if self.state == ActorState::Running {
self.state = ActorState::Stopping;
}
self.inner.stop();
}
/// Terminate actor execution
fn terminate(&mut self) {
self.address.close();
self.items.close();
self.state = ActorState::Stopped;
self.inner.terminate()
}
/// Actor execution state
fn state(&self) -> ActorState {
self.state
self.inner.state()
}
}
impl<A> AsyncContext<A> for HttpContext<A> where A: Actor<Context=Self> + Route
impl<A, S> AsyncContext<A> for HttpContext<A, S>
where
A: Actor<Context = Self>,
{
#[inline]
fn spawn<F>(&mut self, fut: F) -> SpawnHandle
where F: ActorFuture<Item=(), Error=(), Actor=A> + 'static
where
F: ActorFuture<Item = (), Error = (), Actor = A> + 'static,
{
self.modified = true;
self.items.spawn(fut)
self.inner.spawn(fut)
}
#[inline]
fn wait<F>(&mut self, fut: F)
where F: ActorFuture<Item=(), Error=(), Actor=A> + 'static
where
F: ActorFuture<Item = (), Error = (), Actor = A> + 'static,
{
self.modified = true;
self.wait.add(fut);
self.inner.wait(fut)
}
#[doc(hidden)]
#[inline]
fn waiting(&self) -> bool {
self.inner.waiting()
|| self.inner.state() == ActorState::Stopping
|| self.inner.state() == ActorState::Stopped
}
#[inline]
fn cancel_future(&mut self, handle: SpawnHandle) -> bool {
self.modified = true;
self.items.cancel_future(handle)
self.inner.cancel_future(handle)
}
fn cancel_future_on_stop(&mut self, handle: SpawnHandle) {
self.items.cancel_future_on_stop(handle)
#[doc(hidden)]
#[inline]
fn unsync_address(&mut self) -> Addr<Unsync, A> {
self.inner.unsync_address()
}
#[doc(hidden)]
#[inline]
fn sync_address(&mut self) -> Addr<Syn, A> {
self.inner.sync_address()
}
}
#[doc(hidden)]
impl<A> AsyncContextApi<A> for HttpContext<A> where A: Actor<Context=Self> + Route {
fn address_cell(&mut self) -> &mut ActorAddressCell<A> {
&mut self.address
impl<A, S: 'static> HttpContext<A, S>
where
A: Actor<Context = Self>,
{
#[inline]
pub fn new(req: HttpRequest<S>, actor: A) -> HttpContext<A, S> {
HttpContext::from_request(req).actor(actor)
}
}
impl<A> HttpContext<A> where A: Actor<Context=Self> + Route {
pub fn new(state: Rc<<A as Route>::State>) -> HttpContext<A>
{
pub fn from_request(req: HttpRequest<S>) -> HttpContext<A, S> {
HttpContext {
act: None,
state: ActorState::Started,
modified: false,
items: ActorItemsCell::default(),
address: ActorAddressCell::default(),
wait: ActorWaitCell::default(),
stream: VecDeque::new(),
app_state: state,
inner: ContextImpl::new(None),
stream: None,
request: req,
disconnected: false,
}
}
pub(crate) fn set_actor(&mut self, act: A) {
self.act = Some(act)
#[inline]
pub fn actor(mut self, actor: A) -> HttpContext<A, S> {
self.inner.set_actor(actor);
self
}
}
impl<A> HttpContext<A> where A: Actor<Context=Self> + Route {
impl<A, S> HttpContext<A, S>
where
A: Actor<Context = Self>,
{
/// Shared application state
pub fn state(&self) -> &<A as Route>::State {
&self.app_state
#[inline]
pub fn state(&self) -> &S {
self.request.state()
}
/// Start response processing
pub fn start<R: Into<HttpResponse>>(&mut self, response: R) {
self.stream.push_back(Frame::Message(response.into()))
/// Incoming request
#[inline]
pub fn request(&mut self) -> &mut HttpRequest<S> {
&mut self.request
}
/// Write payload
pub fn write<B: Into<BinaryBody>>(&mut self, data: B) {
self.stream.push_back(Frame::Payload(Some(data.into())))
#[inline]
pub fn write<B: Into<Binary>>(&mut self, data: B) {
if !self.disconnected {
self.add_frame(Frame::Chunk(Some(data.into())));
} else {
warn!("Trying to write to disconnected response");
}
}
/// Indicate end of streamimng payload. Also this method calls `Self::close`.
/// Indicate end of streaming payload. Also this method calls `Self::close`.
#[inline]
pub fn write_eof(&mut self) {
self.stream.push_back(Frame::Payload(None));
self.stop();
self.add_frame(Frame::Chunk(None));
}
/// Returns drain future
pub fn drain(&mut self) -> Drain<A> {
let fut = Rc::new(RefCell::new(DrainFut::default()));
self.stream.push_back(Frame::Drain(Rc::clone(&fut)));
self.modified = true;
Drain{ a: PhantomData, inner: fut }
let (tx, rx) = oneshot::channel();
self.inner.modify();
self.add_frame(Frame::Drain(tx));
Drain::new(rx)
}
/// Check if connection still open
#[inline]
pub fn connected(&self) -> bool {
!self.disconnected
}
}
impl<A> HttpContext<A> where A: Actor<Context=Self> + Route {
#[doc(hidden)]
pub fn subscriber<M>(&mut self) -> Box<Subscriber<M>>
where A: Handler<M>,
M: ResponseType + 'static,
{
Box::new(self.address.unsync_address())
#[inline]
fn add_frame(&mut self, frame: Frame) {
if self.stream.is_none() {
self.stream = Some(SmallVec::new());
}
if let Some(s) = self.stream.as_mut() {
s.push(frame)
}
self.inner.modify();
}
#[doc(hidden)]
pub fn sync_subscriber<M>(&mut self) -> Box<Subscriber<M> + Send>
where A: Handler<M>,
M: ResponseType + Send + 'static,
M::Item: Send,
M::Error: Send,
{
Box::new(self.address.sync_address())
/// Handle of the running future
///
/// SpawnHandle is the handle returned by `AsyncContext::spawn()` method.
pub fn handle(&self) -> SpawnHandle {
self.inner.curr_handle()
}
}
#[doc(hidden)]
impl<A> Stream for HttpContext<A> where A: Actor<Context=Self> + Route
impl<A, S> ActorHttpContext for HttpContext<A, S>
where
A: Actor<Context = Self>,
S: 'static,
{
type Item = Frame;
type Error = std::io::Error;
#[inline]
fn disconnected(&mut self) {
self.disconnected = true;
self.stop();
}
fn poll(&mut self) -> Poll<Option<Frame>, std::io::Error> {
if self.act.is_none() {
return Ok(Async::NotReady)
}
let act: &mut A = unsafe {
std::mem::transmute(self.act.as_mut().unwrap() as &mut A)
};
let ctx: &mut HttpContext<A> = unsafe {
std::mem::transmute(self as &mut HttpContext<A>)
};
fn poll(&mut self) -> Poll<Option<SmallVec<[Frame; 4]>>, Error> {
let ctx: &mut HttpContext<A, S> =
unsafe { &mut *(self as &mut HttpContext<A, S> as *mut _) };
// update state
match self.state {
ActorState::Started => {
Actor::started(act, ctx);
self.state = ActorState::Running;
},
ActorState::Stopping => {
Actor::stopping(act, ctx);
if self.inner.alive() {
match self.inner.poll(ctx) {
Ok(Async::NotReady) | Ok(Async::Ready(())) => (),
Err(_) => return Err(ErrorInternalServerError("error")),
}
_ => ()
}
let mut prep_stop = false;
loop {
self.modified = false;
// check wait futures
if self.wait.poll(act, ctx) {
// get frame
if let Some(frame) = self.stream.pop_front() {
return Ok(Async::Ready(Some(frame)))
}
return Ok(Async::NotReady)
}
// incoming messages
self.address.poll(act, ctx);
// spawned futures and streams
self.items.poll(act, ctx);
// are we done
if self.modified {
continue
}
// get frame
if let Some(frame) = self.stream.pop_front() {
return Ok(Async::Ready(Some(frame)))
}
// check state
match self.state {
ActorState::Stopped => {
self.state = ActorState::Stopped;
Actor::stopped(act, ctx);
return Ok(Async::Ready(None))
},
ActorState::Stopping => {
if prep_stop {
if self.address.connected() || !self.items.is_empty() {
self.state = ActorState::Running;
continue
} else {
self.state = ActorState::Stopped;
Actor::stopped(act, ctx);
return Ok(Async::Ready(None))
}
} else {
Actor::stopping(act, ctx);
prep_stop = true;
continue
}
},
ActorState::Running => {
if !self.address.connected() && self.items.is_empty() {
self.state = ActorState::Stopping;
Actor::stopping(act, ctx);
prep_stop = true;
continue
}
},
_ => (),
}
return Ok(Async::NotReady)
// frames
if let Some(data) = self.stream.take() {
Ok(Async::Ready(Some(data)))
} else if self.inner.alive() {
Ok(Async::NotReady)
} else {
Ok(Async::Ready(None))
}
}
}
impl<A> ToEnvelope<A> for HttpContext<A>
where A: Actor<Context=HttpContext<A>> + Route,
impl<A, M, S> ToEnvelope<Syn, A, M> for HttpContext<A, S>
where
A: Actor<Context = HttpContext<A, S>> + Handler<M>,
M: Message + Send + 'static,
M::Result: Send,
{
fn pack<M>(msg: M, tx: Option<Sender<Result<M::Item, M::Error>>>) -> Envelope<A>
where A: Handler<M>,
M: ResponseType + Send + 'static,
M::Item: Send,
M::Error: Send
{
RemoteEnvelope::new(msg, tx).into()
fn pack(msg: M, tx: Option<Sender<M::Result>>) -> SyncEnvelope<A> {
SyncEnvelope::new(msg, tx)
}
}
impl<A, S> From<HttpContext<A, S>> for Body
where
A: Actor<Context = HttpContext<A, S>>,
S: 'static,
{
fn from(ctx: HttpContext<A, S>) -> Body {
Body::Actor(Box::new(ctx))
}
}
pub struct Drain<A> {
a: PhantomData<A>,
inner: Rc<RefCell<DrainFut>>
fut: oneshot::Receiver<()>,
_a: PhantomData<A>,
}
impl<A> ActorFuture for Drain<A>
where A: Actor
{
impl<A> Drain<A> {
pub fn new(fut: oneshot::Receiver<()>) -> Self {
Drain {
fut,
_a: PhantomData,
}
}
}
impl<A: Actor> ActorFuture for Drain<A> {
type Item = ();
type Error = ();
type Actor = A;
fn poll(&mut self, _: &mut A, _: &mut <Self::Actor as Actor>::Context) -> Poll<(), ()> {
self.inner.borrow_mut().poll()
#[inline]
fn poll(
&mut self, _: &mut A, _: &mut <Self::Actor as Actor>::Context,
) -> Poll<Self::Item, Self::Error> {
self.fut.poll().map_err(|_| ())
}
}

View File

@ -1,69 +0,0 @@
use std::cell::RefCell;
use std::fmt::{self, Write};
use std::str;
use time::{self, Duration};
use bytes::BytesMut;
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
pub const DATE_VALUE_LENGTH: usize = 29;
pub fn extend(dst: &mut BytesMut) {
CACHED.with(|cache| {
let mut cache = cache.borrow_mut();
let now = time::get_time();
if now > cache.next_update {
cache.update(now);
}
dst.extend_from_slice(cache.buffer());
})
}
struct CachedDate {
bytes: [u8; DATE_VALUE_LENGTH],
pos: usize,
next_update: time::Timespec,
}
thread_local!(static CACHED: RefCell<CachedDate> = RefCell::new(CachedDate {
bytes: [0; DATE_VALUE_LENGTH],
pos: 0,
next_update: time::Timespec::new(0, 0),
}));
impl CachedDate {
fn buffer(&self) -> &[u8] {
&self.bytes[..]
}
fn update(&mut self, now: time::Timespec) {
self.pos = 0;
write!(self, "{}", time::at_utc(now).rfc822()).unwrap();
assert_eq!(self.pos, DATE_VALUE_LENGTH);
self.next_update = now + Duration::seconds(1);
self.next_update.nsec = 0;
}
}
impl fmt::Write for CachedDate {
fn write_str(&mut self, s: &str) -> fmt::Result {
let len = s.len();
self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes());
self.pos += len;
Ok(())
}
}
#[test]
fn test_date_len() {
assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len());
}
#[test]
fn test_date() {
let mut buf1 = BytesMut::new();
extend(&mut buf1);
let mut buf2 = BytesMut::new();
extend(&mut buf2);
assert_eq!(buf1, buf2);
}

450
src/de.rs Normal file
View File

@ -0,0 +1,450 @@
use serde::de::{self, Deserializer, Error as DeError, Visitor};
use std::borrow::Cow;
use std::convert::AsRef;
use std::slice::Iter;
use httprequest::HttpRequest;
macro_rules! unsupported_type {
($trait_fn:ident, $name:expr) => {
fn $trait_fn<V>(self, _: V) -> Result<V::Value, Self::Error>
where V: Visitor<'de>
{
Err(de::value::Error::custom(concat!("unsupported type: ", $name)))
}
};
}
macro_rules! parse_single_value {
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where V: Visitor<'de>
{
if self.req.match_info().len() != 1 {
Err(de::value::Error::custom(
format!("wrong number of parameters: {} expected 1",
self.req.match_info().len()).as_str()))
} else {
let v = self.req.match_info()[0].parse().map_err(
|_| de::value::Error::custom(
format!("can not parse {:?} to a {}",
&self.req.match_info()[0], $tp)))?;
visitor.$visit_fn(v)
}
}
}
}
pub struct PathDeserializer<'de, S: 'de> {
req: &'de HttpRequest<S>,
}
impl<'de, S: 'de> PathDeserializer<'de, S> {
pub fn new(req: &'de HttpRequest<S>) -> Self {
PathDeserializer { req }
}
}
impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> {
type Error = de::value::Error;
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_map(ParamsDeserializer {
params: self.req.match_info().iter(),
current: None,
})
}
fn deserialize_struct<V>(
self, _: &'static str, _: &'static [&'static str], visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_map(visitor)
}
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_unit_struct<V>(
self, _: &'static str, visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
self.deserialize_unit(visitor)
}
fn deserialize_newtype_struct<V>(
self, _: &'static str, visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_newtype_struct(self)
}
fn deserialize_tuple<V>(
self, len: usize, visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
if self.req.match_info().len() < len {
Err(de::value::Error::custom(
format!(
"wrong number of parameters: {} expected {}",
self.req.match_info().len(),
len
).as_str(),
))
} else {
visitor.visit_seq(ParamsSeq {
params: self.req.match_info().iter(),
})
}
}
fn deserialize_tuple_struct<V>(
self, _: &'static str, len: usize, visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
if self.req.match_info().len() < len {
Err(de::value::Error::custom(
format!(
"wrong number of parameters: {} expected {}",
self.req.match_info().len(),
len
).as_str(),
))
} else {
visitor.visit_seq(ParamsSeq {
params: self.req.match_info().iter(),
})
}
}
fn deserialize_enum<V>(
self, _: &'static str, _: &'static [&'static str], _: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(de::value::Error::custom("unsupported type: enum"))
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
if self.req.match_info().len() != 1 {
Err(de::value::Error::custom(
format!(
"wrong number of parameters: {} expected 1",
self.req.match_info().len()
).as_str(),
))
} else {
visitor.visit_str(&self.req.match_info()[0])
}
}
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_seq(ParamsSeq {
params: self.req.match_info().iter(),
})
}
unsupported_type!(deserialize_any, "'any'");
unsupported_type!(deserialize_bytes, "bytes");
unsupported_type!(deserialize_option, "Option<T>");
unsupported_type!(deserialize_identifier, "identifier");
unsupported_type!(deserialize_ignored_any, "ignored_any");
parse_single_value!(deserialize_bool, visit_bool, "bool");
parse_single_value!(deserialize_i8, visit_i8, "i8");
parse_single_value!(deserialize_i16, visit_i16, "i16");
parse_single_value!(deserialize_i32, visit_i32, "i16");
parse_single_value!(deserialize_i64, visit_i64, "i64");
parse_single_value!(deserialize_u8, visit_u8, "u8");
parse_single_value!(deserialize_u16, visit_u16, "u16");
parse_single_value!(deserialize_u32, visit_u32, "u32");
parse_single_value!(deserialize_u64, visit_u64, "u64");
parse_single_value!(deserialize_f32, visit_f32, "f32");
parse_single_value!(deserialize_f64, visit_f64, "f64");
parse_single_value!(deserialize_string, visit_string, "String");
parse_single_value!(deserialize_byte_buf, visit_string, "String");
parse_single_value!(deserialize_char, visit_char, "char");
}
struct ParamsDeserializer<'de> {
params: Iter<'de, (Cow<'de, str>, Cow<'de, str>)>,
current: Option<(&'de str, &'de str)>,
}
impl<'de> de::MapAccess<'de> for ParamsDeserializer<'de> {
type Error = de::value::Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: de::DeserializeSeed<'de>,
{
self.current = self
.params
.next()
.map(|&(ref k, ref v)| (k.as_ref(), v.as_ref()));
match self.current {
Some((key, _)) => Ok(Some(seed.deserialize(Key { key })?)),
None => Ok(None),
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: de::DeserializeSeed<'de>,
{
if let Some((_, value)) = self.current.take() {
seed.deserialize(Value { value })
} else {
Err(de::value::Error::custom("unexpected item"))
}
}
}
struct Key<'de> {
key: &'de str,
}
impl<'de> Deserializer<'de> for Key<'de> {
type Error = de::value::Error;
fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_str(self.key)
}
fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(de::value::Error::custom("Unexpected"))
}
forward_to_deserialize_any! {
bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes
byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map struct enum ignored_any
}
}
macro_rules! parse_value {
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where V: Visitor<'de>
{
let v = self.value.parse().map_err(
|_| de::value::Error::custom(
format!("can not parse {:?} to a {}", self.value, $tp)))?;
visitor.$visit_fn(v)
}
}
}
struct Value<'de> {
value: &'de str,
}
impl<'de> Deserializer<'de> for Value<'de> {
type Error = de::value::Error;
parse_value!(deserialize_bool, visit_bool, "bool");
parse_value!(deserialize_i8, visit_i8, "i8");
parse_value!(deserialize_i16, visit_i16, "i16");
parse_value!(deserialize_i32, visit_i32, "i16");
parse_value!(deserialize_i64, visit_i64, "i64");
parse_value!(deserialize_u8, visit_u8, "u8");
parse_value!(deserialize_u16, visit_u16, "u16");
parse_value!(deserialize_u32, visit_u32, "u32");
parse_value!(deserialize_u64, visit_u64, "u64");
parse_value!(deserialize_f32, visit_f32, "f32");
parse_value!(deserialize_f64, visit_f64, "f64");
parse_value!(deserialize_string, visit_string, "String");
parse_value!(deserialize_byte_buf, visit_string, "String");
parse_value!(deserialize_char, visit_char, "char");
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_unit_struct<V>(
self, _: &'static str, visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_borrowed_bytes(self.value.as_bytes())
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_borrowed_str(self.value)
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_some(self)
}
fn deserialize_enum<V>(
self, _: &'static str, _: &'static [&'static str], visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_enum(ValueEnum { value: self.value })
}
fn deserialize_newtype_struct<V>(
self, _: &'static str, visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_newtype_struct(self)
}
fn deserialize_tuple<V>(self, _: usize, _: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(de::value::Error::custom("unsupported type: tuple"))
}
fn deserialize_struct<V>(
self, _: &'static str, _: &'static [&'static str], _: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(de::value::Error::custom("unsupported type: struct"))
}
fn deserialize_tuple_struct<V>(
self, _: &'static str, _: usize, _: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(de::value::Error::custom("unsupported type: tuple struct"))
}
unsupported_type!(deserialize_any, "any");
unsupported_type!(deserialize_seq, "seq");
unsupported_type!(deserialize_map, "map");
unsupported_type!(deserialize_identifier, "identifier");
}
struct ParamsSeq<'de> {
params: Iter<'de, (Cow<'de, str>, Cow<'de, str>)>,
}
impl<'de> de::SeqAccess<'de> for ParamsSeq<'de> {
type Error = de::value::Error;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
where
T: de::DeserializeSeed<'de>,
{
match self.params.next() {
Some(item) => Ok(Some(seed.deserialize(Value {
value: item.1.as_ref(),
})?)),
None => Ok(None),
}
}
}
struct ValueEnum<'de> {
value: &'de str,
}
impl<'de> de::EnumAccess<'de> for ValueEnum<'de> {
type Error = de::value::Error;
type Variant = UnitVariant;
fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error>
where
V: de::DeserializeSeed<'de>,
{
Ok((seed.deserialize(Key { key: self.value })?, UnitVariant))
}
}
struct UnitVariant;
impl<'de> de::VariantAccess<'de> for UnitVariant {
type Error = de::value::Error;
fn unit_variant(self) -> Result<(), Self::Error> {
Ok(())
}
fn newtype_variant_seed<T>(self, _seed: T) -> Result<T::Value, Self::Error>
where
T: de::DeserializeSeed<'de>,
{
Err(de::value::Error::custom("not supported"))
}
fn tuple_variant<V>(self, _len: usize, _visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(de::value::Error::custom("not supported"))
}
fn struct_variant<V>(
self, _: &'static [&'static str], _: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(de::value::Error::custom("not supported"))
}
}

View File

@ -1,271 +0,0 @@
#![allow(dead_code)]
use std::{io, usize};
use futures::{Async, Poll};
use bytes::{Bytes, BytesMut};
use self::Kind::{Length, Chunked, Eof};
/// Decoders to handle different Transfer-Encodings.
///
/// If a message body does not include a Transfer-Encoding, it *should*
/// include a Content-Length header.
#[derive(Debug, Clone, PartialEq)]
pub struct Decoder {
kind: Kind,
}
impl Decoder {
pub fn length(x: u64) -> Decoder {
Decoder { kind: Kind::Length(x) }
}
pub fn chunked() -> Decoder {
Decoder { kind: Kind::Chunked(ChunkedState::Size, 0) }
}
pub fn eof() -> Decoder {
Decoder { kind: Kind::Eof(false) }
}
}
#[derive(Debug, Clone, PartialEq)]
enum Kind {
/// A Reader used when a Content-Length header is passed with a positive integer.
Length(u64),
/// A Reader used when Transfer-Encoding is `chunked`.
Chunked(ChunkedState, u64),
/// A Reader used for responses that don't indicate a length or chunked.
///
/// Note: This should only used for `Response`s. It is illegal for a
/// `Request` to be made with both `Content-Length` and
/// `Transfer-Encoding: chunked` missing, as explained from the spec:
///
/// > If a Transfer-Encoding header field is present in a response and
/// > the chunked transfer coding is not the final encoding, the
/// > message body length is determined by reading the connection until
/// > it is closed by the server. If a Transfer-Encoding header field
/// > is present in a request and the chunked transfer coding is not
/// > the final encoding, the message body length cannot be determined
/// > reliably; the server MUST respond with the 400 (Bad Request)
/// > status code and then close the connection.
Eof(bool),
}
#[derive(Debug, PartialEq, Clone)]
enum ChunkedState {
Size,
SizeLws,
Extension,
SizeLf,
Body,
BodyCr,
BodyLf,
EndCr,
EndLf,
End,
}
impl Decoder {
pub fn is_eof(&self) -> bool {
trace!("is_eof? {:?}", self);
match self.kind {
Length(0) |
Chunked(ChunkedState::End, _) |
Eof(true) => true,
_ => false,
}
}
}
impl Decoder {
pub fn decode(&mut self, body: &mut BytesMut) -> Poll<Option<Bytes>, io::Error> {
match self.kind {
Length(ref mut remaining) => {
trace!("Sized read, remaining={:?}", remaining);
if *remaining == 0 {
Ok(Async::Ready(None))
} else {
if body.is_empty() {
return Ok(Async::NotReady)
}
let len = body.len() as u64;
let buf;
if *remaining > len {
buf = body.take().freeze();
*remaining -= len;
} else {
buf = body.split_to(*remaining as usize).freeze();
*remaining = 0;
}
trace!("Length read: {}", buf.len());
Ok(Async::Ready(Some(buf)))
}
}
Chunked(ref mut state, ref mut size) => {
loop {
let mut buf = None;
// advances the chunked state
*state = try_ready!(state.step(body, size, &mut buf));
if *state == ChunkedState::End {
trace!("End of chunked stream");
return Ok(Async::Ready(None));
}
if let Some(buf) = buf {
return Ok(Async::Ready(Some(buf)));
}
if body.is_empty() {
return Ok(Async::NotReady);
}
}
}
Eof(ref mut is_eof) => {
if *is_eof {
Ok(Async::Ready(None))
} else if !body.is_empty() {
Ok(Async::Ready(Some(body.take().freeze())))
} else {
Ok(Async::NotReady)
}
}
}
}
}
macro_rules! byte (
($rdr:ident) => ({
if $rdr.len() > 0 {
let b = $rdr[0];
$rdr.split_to(1);
b
} else {
return Ok(Async::NotReady)
}
})
);
impl ChunkedState {
fn step(&self, body: &mut BytesMut, size: &mut u64, buf: &mut Option<Bytes>)
-> Poll<ChunkedState, io::Error>
{
use self::ChunkedState::*;
match *self {
Size => ChunkedState::read_size(body, size),
SizeLws => ChunkedState::read_size_lws(body),
Extension => ChunkedState::read_extension(body),
SizeLf => ChunkedState::read_size_lf(body, size),
Body => ChunkedState::read_body(body, size, buf),
BodyCr => ChunkedState::read_body_cr(body),
BodyLf => ChunkedState::read_body_lf(body),
EndCr => ChunkedState::read_end_cr(body),
EndLf => ChunkedState::read_end_lf(body),
End => Ok(Async::Ready(ChunkedState::End)),
}
}
fn read_size(rdr: &mut BytesMut, size: &mut u64) -> Poll<ChunkedState, io::Error> {
trace!("Read chunk hex size");
let radix = 16;
match byte!(rdr) {
b @ b'0'...b'9' => {
*size *= radix;
*size += u64::from(b - b'0');
}
b @ b'a'...b'f' => {
*size *= radix;
*size += u64::from(b + 10 - b'a');
}
b @ b'A'...b'F' => {
*size *= radix;
*size += u64::from(b + 10 - b'A');
}
b'\t' | b' ' => return Ok(Async::Ready(ChunkedState::SizeLws)),
b';' => return Ok(Async::Ready(ChunkedState::Extension)),
b'\r' => return Ok(Async::Ready(ChunkedState::SizeLf)),
_ => {
return Err(io::Error::new(io::ErrorKind::InvalidInput,
"Invalid chunk size line: Invalid Size"));
}
}
Ok(Async::Ready(ChunkedState::Size))
}
fn read_size_lws(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> {
trace!("read_size_lws");
match byte!(rdr) {
// LWS can follow the chunk size, but no more digits can come
b'\t' | b' ' => Ok(Async::Ready(ChunkedState::SizeLws)),
b';' => Ok(Async::Ready(ChunkedState::Extension)),
b'\r' => Ok(Async::Ready(ChunkedState::SizeLf)),
_ => {
Err(io::Error::new(io::ErrorKind::InvalidInput,
"Invalid chunk size linear white space"))
}
}
}
fn read_extension(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> {
trace!("read_extension");
match byte!(rdr) {
b'\r' => Ok(Async::Ready(ChunkedState::SizeLf)),
_ => Ok(Async::Ready(ChunkedState::Extension)), // no supported extensions
}
}
fn read_size_lf(rdr: &mut BytesMut, size: &mut u64) -> Poll<ChunkedState, io::Error> {
trace!("Chunk size is {:?}", size);
match byte!(rdr) {
b'\n' if *size > 0 => Ok(Async::Ready(ChunkedState::Body)),
b'\n' if *size == 0 => Ok(Async::Ready(ChunkedState::EndCr)),
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid chunk size LF")),
}
}
fn read_body(rdr: &mut BytesMut, rem: &mut u64, buf: &mut Option<Bytes>)
-> Poll<ChunkedState, io::Error>
{
trace!("Chunked read, remaining={:?}", rem);
let len = rdr.len() as u64;
if len == 0 {
Ok(Async::Ready(ChunkedState::Body))
} else {
let slice;
if *rem > len {
slice = rdr.take().freeze();
*rem -= len;
} else {
slice = rdr.split_to(*rem as usize).freeze();
*rem = 0;
}
*buf = Some(slice);
if *rem > 0 {
Ok(Async::Ready(ChunkedState::Body))
} else {
Ok(Async::Ready(ChunkedState::BodyCr))
}
}
}
fn read_body_cr(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> {
match byte!(rdr) {
b'\r' => Ok(Async::Ready(ChunkedState::BodyLf)),
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid chunk body CR")),
}
}
fn read_body_lf(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> {
match byte!(rdr) {
b'\n' => Ok(Async::Ready(ChunkedState::Size)),
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid chunk body LF")),
}
}
fn read_end_cr(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> {
match byte!(rdr) {
b'\r' => Ok(Async::Ready(ChunkedState::EndLf)),
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid chunk end CR")),
}
}
fn read_end_lf(rdr: &mut BytesMut) -> Poll<ChunkedState, io::Error> {
match byte!(rdr) {
b'\n' => Ok(Async::Ready(ChunkedState::End)),
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid chunk end LF")),
}
}
}

View File

@ -1,15 +0,0 @@
//! The `actix-web` prelude for library developers
//!
//! The purpose of this module is to alleviate imports of many common actix traits
//! by adding a glob import to the top of actix heavy modules:
//!
//! ```
//! # #![allow(unused_imports)]
//! use actix_web::dev::*;
//! ```
pub use super::*;
// dev specific
pub use task::Task;
pub use recognizer::RouteRecognizer;

File diff suppressed because it is too large Load Diff

740
src/extractor.rs Normal file
View File

@ -0,0 +1,740 @@
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::str;
use bytes::Bytes;
use encoding::all::UTF_8;
use encoding::types::{DecoderTrap, Encoding};
use futures::{Async, Future, Poll};
use mime::Mime;
use serde::de::{self, DeserializeOwned};
use serde_urlencoded;
use de::PathDeserializer;
use error::{Error, ErrorBadRequest, ErrorNotFound};
use handler::{AsyncResult, FromRequest};
use httpmessage::{HttpMessage, MessageBody, UrlEncoded};
use httprequest::HttpRequest;
/// Extract typed information from the request's path.
///
/// ## Example
///
/// ```rust
/// # extern crate bytes;
/// # extern crate actix_web;
/// # extern crate futures;
/// use actix_web::{App, Path, Result, http};
///
/// /// extract path info from "/{username}/{count}/index.html" url
/// /// {username} - deserializes to a String
/// /// {count} - - deserializes to a u32
/// fn index(info: Path<(String, u32)>) -> Result<String> {
/// Ok(format!("Welcome {}! {}", info.0, info.1))
/// }
///
/// fn main() {
/// let app = App::new().resource(
/// "/{username}/{count}/index.html", // <- define path parameters
/// |r| r.method(http::Method::GET).with(index)); // <- use `with` extractor
/// }
/// ```
///
/// It is possible to extract path information to a specific type that
/// implements `Deserialize` trait from *serde*.
///
/// ```rust
/// # extern crate bytes;
/// # extern crate actix_web;
/// # extern crate futures;
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{App, Path, Result, http};
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// extract path info using serde
/// fn index(info: Path<Info>) -> Result<String> {
/// Ok(format!("Welcome {}!", info.username))
/// }
///
/// fn main() {
/// let app = App::new().resource(
/// "/{username}/index.html", // <- define path parameters
/// |r| r.method(http::Method::GET).with(index)); // <- use `with` extractor
/// }
/// ```
pub struct Path<T> {
inner: T,
}
impl<T> AsRef<T> for Path<T> {
fn as_ref(&self) -> &T {
&self.inner
}
}
impl<T> Deref for Path<T> {
type Target = T;
fn deref(&self) -> &T {
&self.inner
}
}
impl<T> DerefMut for Path<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.inner
}
}
impl<T> Path<T> {
/// Deconstruct to an inner value
pub fn into_inner(self) -> T {
self.inner
}
}
impl<T, S> FromRequest<S> for Path<T>
where
T: DeserializeOwned,
{
type Config = ();
type Result = Result<Self, Error>;
#[inline]
fn from_request(req: &HttpRequest<S>, _: &Self::Config) -> Self::Result {
let req = req.clone();
de::Deserialize::deserialize(PathDeserializer::new(&req))
.map_err(ErrorNotFound)
.map(|inner| Path { inner })
}
}
/// Extract typed information from from the request's query.
///
/// ## Example
///
/// ```rust
/// # extern crate bytes;
/// # extern crate actix_web;
/// # extern crate futures;
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{App, Query, http};
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// // use `with` extractor for query info
/// // this handler get called only if request's query contains `username` field
/// fn index(info: Query<Info>) -> String {
/// format!("Welcome {}!", info.username)
/// }
///
/// fn main() {
/// let app = App::new().resource(
/// "/index.html",
/// |r| r.method(http::Method::GET).with(index)); // <- use `with` extractor
/// }
/// ```
pub struct Query<T>(T);
impl<T> Deref for Query<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> DerefMut for Query<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T> Query<T> {
/// Deconstruct to a inner value
pub fn into_inner(self) -> T {
self.0
}
}
impl<T, S> FromRequest<S> for Query<T>
where
T: de::DeserializeOwned,
{
type Config = ();
type Result = Result<Self, Error>;
#[inline]
fn from_request(req: &HttpRequest<S>, _: &Self::Config) -> Self::Result {
let req = req.clone();
serde_urlencoded::from_str::<T>(req.query_string())
.map_err(|e| e.into())
.map(Query)
}
}
/// Extract typed information from the request's body.
///
/// To extract typed information from request's body, the type `T` must
/// implement the `Deserialize` trait from *serde*.
///
/// [**FormConfig**](dev/struct.FormConfig.html) allows to configure extraction
/// process.
///
/// ## Example
///
/// ```rust
/// # extern crate actix_web;
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{App, Form, Result};
///
/// #[derive(Deserialize)]
/// struct FormData {
/// username: String,
/// }
///
/// /// extract form data using serde
/// /// this handler get called only if content type is *x-www-form-urlencoded*
/// /// and content of the request could be deserialized to a `FormData` struct
/// fn index(form: Form<FormData>) -> Result<String> {
/// Ok(format!("Welcome {}!", form.username))
/// }
/// # fn main() {}
/// ```
pub struct Form<T>(pub T);
impl<T> Form<T> {
/// Deconstruct to an inner value
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> Deref for Form<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> DerefMut for Form<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T, S> FromRequest<S> for Form<T>
where
T: DeserializeOwned + 'static,
S: 'static,
{
type Config = FormConfig;
type Result = Box<Future<Item = Self, Error = Error>>;
#[inline]
fn from_request(req: &HttpRequest<S>, cfg: &Self::Config) -> Self::Result {
Box::new(
UrlEncoded::new(req.clone())
.limit(cfg.limit)
.from_err()
.map(Form),
)
}
}
/// Form extractor configuration
///
/// ```rust
/// # extern crate actix_web;
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{App, Form, Result, http};
///
/// #[derive(Deserialize)]
/// struct FormData {
/// username: String,
/// }
///
/// /// extract form data using serde.
/// /// custom configuration is used for this handler, max payload size is 4k
/// fn index(form: Form<FormData>) -> Result<String> {
/// Ok(format!("Welcome {}!", form.username))
/// }
///
/// fn main() {
/// let app = App::new().resource(
/// "/index.html", |r| {
/// r.method(http::Method::GET)
/// .with(index)
/// .limit(4096);} // <- change form extractor configuration
/// );
/// }
/// ```
pub struct FormConfig {
limit: usize,
}
impl FormConfig {
/// Change max size of payload. By default max size is 256Kb
pub fn limit(&mut self, limit: usize) -> &mut Self {
self.limit = limit;
self
}
}
impl Default for FormConfig {
fn default() -> Self {
FormConfig { limit: 262_144 }
}
}
/// Request payload extractor.
///
/// Loads request's payload and construct Bytes instance.
///
/// [**PayloadConfig**](dev/struct.PayloadConfig.html) allows to configure
/// extraction process.
///
/// ## Example
///
/// ```rust
/// extern crate bytes;
/// # extern crate actix_web;
/// use actix_web::{http, App, Result};
///
/// /// extract text data from request
/// fn index(body: bytes::Bytes) -> Result<String> {
/// Ok(format!("Body {:?}!", body))
/// }
///
/// fn main() {
/// let app = App::new().resource(
/// "/index.html", |r|
/// r.method(http::Method::GET).with(index));
/// }
/// ```
impl<S: 'static> FromRequest<S> for Bytes {
type Config = PayloadConfig;
type Result = Result<Box<Future<Item = Self, Error = Error>>, Error>;
#[inline]
fn from_request(req: &HttpRequest<S>, cfg: &Self::Config) -> Self::Result {
// check content-type
cfg.check_mimetype(req)?;
Ok(Box::new(
MessageBody::new(req.clone()).limit(cfg.limit).from_err(),
))
}
}
/// Extract text information from the request's body.
///
/// Text extractor automatically decode body according to the request's charset.
///
/// [**PayloadConfig**](dev/struct.PayloadConfig.html) allows to configure
/// extraction process.
///
/// ## Example
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::{http, App, Result};
///
/// /// extract text data from request
/// fn index(body: String) -> Result<String> {
/// Ok(format!("Body {}!", body))
/// }
///
/// fn main() {
/// let app = App::new().resource(
/// "/index.html", |r| {
/// r.method(http::Method::GET)
/// .with(index) // <- register handler with extractor params
/// .limit(4096); // <- limit size of the payload
/// });
/// }
/// ```
impl<S: 'static> FromRequest<S> for String {
type Config = PayloadConfig;
type Result = Result<Box<Future<Item = String, Error = Error>>, Error>;
#[inline]
fn from_request(req: &HttpRequest<S>, cfg: &Self::Config) -> Self::Result {
// check content-type
cfg.check_mimetype(req)?;
// check charset
let encoding = req.encoding()?;
Ok(Box::new(
MessageBody::new(req.clone())
.limit(cfg.limit)
.from_err()
.and_then(move |body| {
let enc: *const Encoding = encoding as *const Encoding;
if enc == UTF_8 {
Ok(str::from_utf8(body.as_ref())
.map_err(|_| ErrorBadRequest("Can not decode body"))?
.to_owned())
} else {
Ok(encoding
.decode(&body, DecoderTrap::Strict)
.map_err(|_| ErrorBadRequest("Can not decode body"))?)
}
}),
))
}
}
/// Payload configuration for request's payload.
pub struct PayloadConfig {
limit: usize,
mimetype: Option<Mime>,
}
impl PayloadConfig {
/// Change max size of payload. By default max size is 256Kb
pub fn limit(&mut self, limit: usize) -> &mut Self {
self.limit = limit;
self
}
/// Set required mime-type of the request. By default mime type is not
/// enforced.
pub fn mimetype(&mut self, mt: Mime) -> &mut Self {
self.mimetype = Some(mt);
self
}
fn check_mimetype<S>(&self, req: &HttpRequest<S>) -> Result<(), Error> {
// check content-type
if let Some(ref mt) = self.mimetype {
match req.mime_type() {
Ok(Some(ref req_mt)) => {
if mt != req_mt {
return Err(ErrorBadRequest("Unexpected Content-Type"));
}
}
Ok(None) => {
return Err(ErrorBadRequest("Content-Type is expected"));
}
Err(err) => {
return Err(err.into());
}
}
}
Ok(())
}
}
impl Default for PayloadConfig {
fn default() -> Self {
PayloadConfig {
limit: 262_144,
mimetype: None,
}
}
}
macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => {
/// FromRequest implementation for tuple
impl<S, $($T: FromRequest<S> + 'static),+> FromRequest<S> for ($($T,)+)
where
S: 'static,
{
type Config = ($($T::Config,)+);
type Result = Box<Future<Item = ($($T,)+), Error = Error>>;
fn from_request(req: &HttpRequest<S>, cfg: &Self::Config) -> Self::Result {
Box::new($fut_type {
s: PhantomData,
items: <($(Option<$T>,)+)>::default(),
futs: ($(Some($T::from_request(req, &cfg.$n).into()),)+),
})
}
}
struct $fut_type<S, $($T: FromRequest<S>),+>
where
S: 'static,
{
s: PhantomData<S>,
items: ($(Option<$T>,)+),
futs: ($(Option<AsyncResult<$T>>,)+),
}
impl<S, $($T: FromRequest<S>),+> Future for $fut_type<S, $($T),+>
where
S: 'static,
{
type Item = ($($T,)+);
type Error = Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let mut ready = true;
$(
if self.futs.$n.is_some() {
match self.futs.$n.as_mut().unwrap().poll() {
Ok(Async::Ready(item)) => {
self.items.$n = Some(item);
self.futs.$n.take();
}
Ok(Async::NotReady) => ready = false,
Err(e) => return Err(e),
}
}
)+
if ready {
Ok(Async::Ready(
($(self.items.$n.take().unwrap(),)+)
))
} else {
Ok(Async::NotReady)
}
}
}
});
tuple_from_req!(TupleFromRequest1, (0, A));
tuple_from_req!(TupleFromRequest2, (0, A), (1, B));
tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C));
tuple_from_req!(TupleFromRequest4, (0, A), (1, B), (2, C), (3, D));
tuple_from_req!(TupleFromRequest5, (0, A), (1, B), (2, C), (3, D), (4, E));
tuple_from_req!(
TupleFromRequest6,
(0, A),
(1, B),
(2, C),
(3, D),
(4, E),
(5, F)
);
tuple_from_req!(
TupleFromRequest7,
(0, A),
(1, B),
(2, C),
(3, D),
(4, E),
(5, F),
(6, G)
);
tuple_from_req!(
TupleFromRequest8,
(0, A),
(1, B),
(2, C),
(3, D),
(4, E),
(5, F),
(6, G),
(7, H)
);
tuple_from_req!(
TupleFromRequest9,
(0, A),
(1, B),
(2, C),
(3, D),
(4, E),
(5, F),
(6, G),
(7, H),
(8, I)
);
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use futures::{Async, Future};
use http::header;
use mime;
use resource::ResourceHandler;
use router::{Resource, Router};
use server::ServerSettings;
use test::TestRequest;
#[derive(Deserialize, Debug, PartialEq)]
struct Info {
hello: String,
}
#[test]
fn test_bytes() {
let cfg = PayloadConfig::default();
let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11").finish();
req.payload_mut()
.unread_data(Bytes::from_static(b"hello=world"));
match Bytes::from_request(&req, &cfg).unwrap().poll().unwrap() {
Async::Ready(s) => {
assert_eq!(s, Bytes::from_static(b"hello=world"));
}
_ => unreachable!(),
}
}
#[test]
fn test_string() {
let cfg = PayloadConfig::default();
let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11").finish();
req.payload_mut()
.unread_data(Bytes::from_static(b"hello=world"));
match String::from_request(&req, &cfg).unwrap().poll().unwrap() {
Async::Ready(s) => {
assert_eq!(s, "hello=world");
}
_ => unreachable!(),
}
}
#[test]
fn test_form() {
let mut req = TestRequest::with_header(
header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
).header(header::CONTENT_LENGTH, "11")
.finish();
req.payload_mut()
.unread_data(Bytes::from_static(b"hello=world"));
let mut cfg = FormConfig::default();
cfg.limit(4096);
match Form::<Info>::from_request(&req, &cfg).poll().unwrap() {
Async::Ready(s) => {
assert_eq!(s.hello, "world");
}
_ => unreachable!(),
}
}
#[test]
fn test_payload_config() {
let req = HttpRequest::default();
let mut cfg = PayloadConfig::default();
cfg.mimetype(mime::APPLICATION_JSON);
assert!(cfg.check_mimetype(&req).is_err());
let req = TestRequest::with_header(
header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
).finish();
assert!(cfg.check_mimetype(&req).is_err());
let req =
TestRequest::with_header(header::CONTENT_TYPE, "application/json").finish();
assert!(cfg.check_mimetype(&req).is_ok());
}
#[derive(Deserialize)]
struct MyStruct {
key: String,
value: String,
}
#[derive(Deserialize)]
struct Id {
id: String,
}
#[derive(Deserialize)]
struct Test2 {
key: String,
value: u32,
}
#[test]
fn test_request_extract() {
let mut req = TestRequest::with_uri("/name/user1/?id=test").finish();
let mut resource = ResourceHandler::<()>::default();
resource.name("index");
let mut routes = Vec::new();
routes.push((Resource::new("index", "/{key}/{value}/"), Some(resource)));
let (router, _) = Router::new("", ServerSettings::default(), routes);
assert!(router.recognize(&mut req).is_some());
let s = Path::<MyStruct>::from_request(&req, &()).unwrap();
assert_eq!(s.key, "name");
assert_eq!(s.value, "user1");
let s = Path::<(String, String)>::from_request(&req, &()).unwrap();
assert_eq!(s.0, "name");
assert_eq!(s.1, "user1");
let s = Query::<Id>::from_request(&req, &()).unwrap();
assert_eq!(s.id, "test");
let mut req = TestRequest::with_uri("/name/32/").finish();
assert!(router.recognize(&mut req).is_some());
let s = Path::<Test2>::from_request(&req, &()).unwrap();
assert_eq!(s.as_ref().key, "name");
assert_eq!(s.value, 32);
let s = Path::<(String, u8)>::from_request(&req, &()).unwrap();
assert_eq!(s.0, "name");
assert_eq!(s.1, 32);
let res = Path::<Vec<String>>::extract(&req).unwrap();
assert_eq!(res[0], "name".to_owned());
assert_eq!(res[1], "32".to_owned());
}
#[test]
fn test_extract_path_single() {
let mut resource = ResourceHandler::<()>::default();
resource.name("index");
let mut routes = Vec::new();
routes.push((Resource::new("index", "/{value}/"), Some(resource)));
let (router, _) = Router::new("", ServerSettings::default(), routes);
let mut req = TestRequest::with_uri("/32/").finish();
assert!(router.recognize(&mut req).is_some());
assert_eq!(*Path::<i8>::from_request(&mut req, &()).unwrap(), 32);
}
#[test]
fn test_tuple_extract() {
let mut req = TestRequest::with_uri("/name/user1/?id=test").finish();
let mut resource = ResourceHandler::<()>::default();
resource.name("index");
let mut routes = Vec::new();
routes.push((Resource::new("index", "/{key}/{value}/"), Some(resource)));
let (router, _) = Router::new("", ServerSettings::default(), routes);
assert!(router.recognize(&mut req).is_some());
let res = match <(Path<(String, String)>,)>::extract(&req).poll() {
Ok(Async::Ready(res)) => res,
_ => panic!("error"),
};
assert_eq!((res.0).0, "name");
assert_eq!((res.0).1, "user1");
let res = match <(Path<(String, String)>, Path<(String, String)>)>::extract(&req)
.poll()
{
Ok(Async::Ready(res)) => res,
_ => panic!("error"),
};
assert_eq!((res.0).0, "name");
assert_eq!((res.0).1, "user1");
assert_eq!((res.1).0, "name");
assert_eq!((res.1).1, "user1");
}
}

1132
src/fs.rs Normal file

File diff suppressed because it is too large Load Diff

522
src/handler.rs Normal file
View File

@ -0,0 +1,522 @@
use std::marker::PhantomData;
use std::ops::Deref;
use futures::future::{err, ok, Future};
use futures::{Async, Poll};
use error::Error;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
/// Trait defines object that could be registered as route handler
#[allow(unused_variables)]
pub trait Handler<S>: 'static {
/// The type of value that handler will return.
type Result: Responder;
/// Handle request
fn handle(&mut self, req: HttpRequest<S>) -> Self::Result;
}
/// Trait implemented by types that generate responses for clients.
///
/// Types that implement this trait can be used as the return type of a handler.
pub trait Responder {
/// The associated item which can be returned.
type Item: Into<AsyncResult<HttpResponse>>;
/// The associated error which can be returned.
type Error: Into<Error>;
/// Convert itself to `AsyncResult` or `Error`.
fn respond_to<S: 'static>(
self, req: &HttpRequest<S>,
) -> Result<Self::Item, Self::Error>;
}
/// Trait implemented by types that can be extracted from request.
///
/// Types that implement this trait can be used with `Route::with()` method.
pub trait FromRequest<S>: Sized {
/// Configuration for conversion process
type Config: Default;
/// Future that resolves to a Self
type Result: Into<AsyncResult<Self>>;
/// Convert request to a Self
fn from_request(req: &HttpRequest<S>, cfg: &Self::Config) -> Self::Result;
/// Convert request to a Self
///
/// This method uses default extractor configuration
fn extract(req: &HttpRequest<S>) -> Self::Result {
Self::from_request(req, &Self::Config::default())
}
}
/// Combines two different responder types into a single type
///
/// ```rust
/// # extern crate actix_web;
/// # extern crate futures;
/// # use futures::future::Future;
/// use futures::future::result;
/// use actix_web::{Either, Error, HttpRequest, HttpResponse, AsyncResponder};
///
/// type RegisterResult = Either<HttpResponse, Box<Future<Item=HttpResponse, Error=Error>>>;
///
///
/// fn index(req: HttpRequest) -> RegisterResult {
/// if is_a_variant() { // <- choose variant A
/// Either::A(
/// HttpResponse::BadRequest().body("Bad data"))
/// } else {
/// Either::B( // <- variant B
/// result(Ok(HttpResponse::Ok()
/// .content_type("text/html")
/// .body("Hello!")))
/// .responder())
/// }
/// }
/// # fn is_a_variant() -> bool { true }
/// # fn main() {}
/// ```
#[derive(Debug)]
pub enum Either<A, B> {
/// First branch of the type
A(A),
/// Second branch of the type
B(B),
}
impl<A, B> Responder for Either<A, B>
where
A: Responder,
B: Responder,
{
type Item = AsyncResult<HttpResponse>;
type Error = Error;
fn respond_to<S: 'static>(
self, req: &HttpRequest<S>,
) -> Result<AsyncResult<HttpResponse>, Error> {
match self {
Either::A(a) => match a.respond_to(req) {
Ok(val) => Ok(val.into()),
Err(err) => Err(err.into()),
},
Either::B(b) => match b.respond_to(req) {
Ok(val) => Ok(val.into()),
Err(err) => Err(err.into()),
},
}
}
}
impl<A, B, I, E> Future for Either<A, B>
where
A: Future<Item = I, Error = E>,
B: Future<Item = I, Error = E>,
{
type Item = I;
type Error = E;
fn poll(&mut self) -> Poll<I, E> {
match *self {
Either::A(ref mut fut) => fut.poll(),
Either::B(ref mut fut) => fut.poll(),
}
}
}
/// Convenience trait that converts `Future` object to a `Boxed` future
///
/// For example loading json from request's body is async operation.
///
/// ```rust
/// # extern crate actix_web;
/// # extern crate futures;
/// # #[macro_use] extern crate serde_derive;
/// use futures::future::Future;
/// use actix_web::{
/// App, HttpRequest, HttpResponse, HttpMessage, Error, AsyncResponder};
///
/// #[derive(Deserialize, Debug)]
/// struct MyObj {
/// name: String,
/// }
///
/// fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
/// req.json() // <- get JsonBody future
/// .from_err()
/// .and_then(|val: MyObj| { // <- deserialized value
/// Ok(HttpResponse::Ok().into())
/// })
/// // Construct boxed future by using `AsyncResponder::responder()` method
/// .responder()
/// }
/// # fn main() {}
/// ```
pub trait AsyncResponder<I, E>: Sized {
fn responder(self) -> Box<Future<Item = I, Error = E>>;
}
impl<F, I, E> AsyncResponder<I, E> for F
where
F: Future<Item = I, Error = E> + 'static,
I: Responder + 'static,
E: Into<Error> + 'static,
{
fn responder(self) -> Box<Future<Item = I, Error = E>> {
Box::new(self)
}
}
/// Handler<S> for Fn()
impl<F, R, S> Handler<S> for F
where
F: Fn(HttpRequest<S>) -> R + 'static,
R: Responder + 'static,
{
type Result = R;
fn handle(&mut self, req: HttpRequest<S>) -> R {
(self)(req)
}
}
/// Represents async result
///
/// Result could be in tree different forms.
/// * Ok(T) - ready item
/// * Err(E) - error happen during reply process
/// * Future<T, E> - reply process completes in the future
pub struct AsyncResult<I, E = Error>(Option<AsyncResultItem<I, E>>);
impl<I, E> Future for AsyncResult<I, E> {
type Item = I;
type Error = E;
fn poll(&mut self) -> Poll<I, E> {
let res = self.0.take().expect("use after resolve");
match res {
AsyncResultItem::Ok(msg) => Ok(Async::Ready(msg)),
AsyncResultItem::Err(err) => Err(err),
AsyncResultItem::Future(mut fut) => match fut.poll() {
Ok(Async::NotReady) => {
self.0 = Some(AsyncResultItem::Future(fut));
Ok(Async::NotReady)
}
Ok(Async::Ready(msg)) => Ok(Async::Ready(msg)),
Err(err) => Err(err),
},
}
}
}
pub(crate) enum AsyncResultItem<I, E> {
Ok(I),
Err(E),
Future(Box<Future<Item = I, Error = E>>),
}
impl<I, E> AsyncResult<I, E> {
/// Create async response
#[inline]
pub fn async(fut: Box<Future<Item = I, Error = E>>) -> AsyncResult<I, E> {
AsyncResult(Some(AsyncResultItem::Future(fut)))
}
/// Send response
#[inline]
pub fn ok<R: Into<I>>(ok: R) -> AsyncResult<I, E> {
AsyncResult(Some(AsyncResultItem::Ok(ok.into())))
}
/// Send error
#[inline]
pub fn err<R: Into<E>>(err: R) -> AsyncResult<I, E> {
AsyncResult(Some(AsyncResultItem::Err(err.into())))
}
#[inline]
pub(crate) fn into(self) -> AsyncResultItem<I, E> {
self.0.expect("use after resolve")
}
#[cfg(test)]
pub(crate) fn as_msg(&self) -> &I {
match self.0.as_ref().unwrap() {
&AsyncResultItem::Ok(ref resp) => resp,
_ => panic!(),
}
}
#[cfg(test)]
pub(crate) fn as_err(&self) -> Option<&E> {
match self.0.as_ref().unwrap() {
&AsyncResultItem::Err(ref err) => Some(err),
_ => None,
}
}
}
impl Responder for AsyncResult<HttpResponse> {
type Item = AsyncResult<HttpResponse>;
type Error = Error;
fn respond_to<S>(
self, _: &HttpRequest<S>,
) -> Result<AsyncResult<HttpResponse>, Error> {
Ok(self)
}
}
impl Responder for HttpResponse {
type Item = AsyncResult<HttpResponse>;
type Error = Error;
#[inline]
fn respond_to<S>(
self, _: &HttpRequest<S>,
) -> Result<AsyncResult<HttpResponse>, Error> {
Ok(AsyncResult(Some(AsyncResultItem::Ok(self))))
}
}
impl<T> From<T> for AsyncResult<T> {
#[inline]
fn from(resp: T) -> AsyncResult<T> {
AsyncResult(Some(AsyncResultItem::Ok(resp)))
}
}
impl<T: Responder, E: Into<Error>> Responder for Result<T, E> {
type Item = <T as Responder>::Item;
type Error = Error;
fn respond_to<S: 'static>(self, req: &HttpRequest<S>) -> Result<Self::Item, Error> {
match self {
Ok(val) => match val.respond_to(req) {
Ok(val) => Ok(val),
Err(err) => Err(err.into()),
},
Err(err) => Err(err.into()),
}
}
}
impl<T, E: Into<Error>> From<Result<AsyncResult<T>, E>> for AsyncResult<T> {
#[inline]
fn from(res: Result<AsyncResult<T>, E>) -> Self {
match res {
Ok(val) => val,
Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))),
}
}
}
impl<T, E: Into<Error>> From<Result<T, E>> for AsyncResult<T> {
#[inline]
fn from(res: Result<T, E>) -> Self {
match res {
Ok(val) => AsyncResult(Some(AsyncResultItem::Ok(val))),
Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))),
}
}
}
impl<T, E: Into<Error>> From<Result<Box<Future<Item = T, Error = Error>>, E>>
for AsyncResult<T>
{
#[inline]
fn from(res: Result<Box<Future<Item = T, Error = Error>>, E>) -> Self {
match res {
Ok(fut) => AsyncResult(Some(AsyncResultItem::Future(fut))),
Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))),
}
}
}
impl<T> From<Box<Future<Item = T, Error = Error>>> for AsyncResult<T> {
#[inline]
fn from(fut: Box<Future<Item = T, Error = Error>>) -> AsyncResult<T> {
AsyncResult(Some(AsyncResultItem::Future(fut)))
}
}
/// Convenience type alias
pub type FutureResponse<I, E = Error> = Box<Future<Item = I, Error = E>>;
impl<I, E> Responder for Box<Future<Item = I, Error = E>>
where
I: Responder + 'static,
E: Into<Error> + 'static,
{
type Item = AsyncResult<HttpResponse>;
type Error = Error;
#[inline]
fn respond_to<S: 'static>(
self, req: &HttpRequest<S>,
) -> Result<AsyncResult<HttpResponse>, Error> {
let req = req.clone();
let fut = self
.map_err(|e| e.into())
.then(move |r| match r.respond_to(&req) {
Ok(reply) => match reply.into().into() {
AsyncResultItem::Ok(resp) => ok(resp),
_ => panic!("Nested async replies are not supported"),
},
Err(e) => err(e),
});
Ok(AsyncResult::async(Box::new(fut)))
}
}
// /// Trait defines object that could be registered as resource route
pub(crate) trait RouteHandler<S>: 'static {
fn handle(&mut self, req: HttpRequest<S>) -> AsyncResult<HttpResponse>;
}
/// Route handler wrapper for Handler
pub(crate) struct WrapHandler<S, H, R>
where
H: Handler<S, Result = R>,
R: Responder,
S: 'static,
{
h: H,
s: PhantomData<S>,
}
impl<S, H, R> WrapHandler<S, H, R>
where
H: Handler<S, Result = R>,
R: Responder,
S: 'static,
{
pub fn new(h: H) -> Self {
WrapHandler { h, s: PhantomData }
}
}
impl<S, H, R> RouteHandler<S> for WrapHandler<S, H, R>
where
H: Handler<S, Result = R>,
R: Responder + 'static,
S: 'static,
{
fn handle(&mut self, req: HttpRequest<S>) -> AsyncResult<HttpResponse> {
match self.h.handle(req.clone()).respond_to(&req) {
Ok(reply) => reply.into(),
Err(err) => AsyncResult::err(err.into()),
}
}
}
/// Async route handler
pub(crate) struct AsyncHandler<S, H, F, R, E>
where
H: Fn(HttpRequest<S>) -> F + 'static,
F: Future<Item = R, Error = E> + 'static,
R: Responder + 'static,
E: Into<Error> + 'static,
S: 'static,
{
h: Box<H>,
s: PhantomData<S>,
}
impl<S, H, F, R, E> AsyncHandler<S, H, F, R, E>
where
H: Fn(HttpRequest<S>) -> F + 'static,
F: Future<Item = R, Error = E> + 'static,
R: Responder + 'static,
E: Into<Error> + 'static,
S: 'static,
{
pub fn new(h: H) -> Self {
AsyncHandler {
h: Box::new(h),
s: PhantomData,
}
}
}
impl<S, H, F, R, E> RouteHandler<S> for AsyncHandler<S, H, F, R, E>
where
H: Fn(HttpRequest<S>) -> F + 'static,
F: Future<Item = R, Error = E> + 'static,
R: Responder + 'static,
E: Into<Error> + 'static,
S: 'static,
{
fn handle(&mut self, req: HttpRequest<S>) -> AsyncResult<HttpResponse> {
let fut = (self.h)(req.clone()).map_err(|e| e.into()).then(move |r| {
match r.respond_to(&req) {
Ok(reply) => match reply.into().into() {
AsyncResultItem::Ok(resp) => Either::A(ok(resp)),
AsyncResultItem::Err(e) => Either::A(err(e)),
AsyncResultItem::Future(fut) => Either::B(fut),
},
Err(e) => Either::A(err(e)),
}
});
AsyncResult::async(Box::new(fut))
}
}
/// Access an application state
///
/// `S` - application state type
///
/// ## Example
///
/// ```rust
/// # extern crate bytes;
/// # extern crate actix_web;
/// # extern crate futures;
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{App, Path, State, http};
///
/// /// Application state
/// struct MyApp {msg: &'static str}
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// extract path info using serde
/// fn index(data: (State<MyApp>, Path<Info>)) -> String {
/// let (state, path) = data;
/// format!("{} {}!", state.msg, path.username)
/// }
///
/// fn main() {
/// let app = App::with_state(MyApp{msg: "Welcome"}).resource(
/// "/{username}/index.html", // <- define path parameters
/// |r| r.method(http::Method::GET).with(index)); // <- use `with` extractor
/// }
/// ```
pub struct State<S>(HttpRequest<S>);
impl<S> Deref for State<S> {
type Target = S;
fn deref(&self) -> &S {
self.0.state()
}
}
impl<S> FromRequest<S> for State<S> {
type Config = ();
type Result = State<S>;
#[inline]
fn from_request(req: &HttpRequest<S>, _: &Self::Config) -> Self::Result {
State(req.clone())
}
}

159
src/header/common/accept.rs Normal file
View File

@ -0,0 +1,159 @@
use header::{qitem, QualityItem};
use http::header as http;
use mime::{self, Mime};
header! {
/// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2)
///
/// The `Accept` header field can be used by user agents to specify
/// response media types that are acceptable. Accept header fields can
/// be used to indicate that the request is specifically limited to a
/// small set of desired types, as in the case of a request for an
/// in-line image
///
/// # ABNF
///
/// ```text
/// Accept = #( media-range [ accept-params ] )
///
/// media-range = ( "*/*"
/// / ( type "/" "*" )
/// / ( type "/" subtype )
/// ) *( OWS ";" OWS parameter )
/// accept-params = weight *( accept-ext )
/// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ]
/// ```
///
/// # Example values
/// * `audio/*; q=0.2, audio/basic`
/// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`
///
/// # Examples
/// ```rust
/// # extern crate actix_web;
/// extern crate mime;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{Accept, qitem};
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
///
/// builder.set(
/// Accept(vec![
/// qitem(mime::TEXT_HTML),
/// ])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate actix_web;
/// extern crate mime;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{Accept, qitem};
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
///
/// builder.set(
/// Accept(vec![
/// qitem(mime::APPLICATION_JSON),
/// ])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate actix_web;
/// extern crate mime;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{Accept, QualityItem, q, qitem};
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
///
/// builder.set(
/// Accept(vec![
/// qitem(mime::TEXT_HTML),
/// qitem("application/xhtml+xml".parse().unwrap()),
/// QualityItem::new(
/// mime::TEXT_XML,
/// q(900)
/// ),
/// qitem("image/webp".parse().unwrap()),
/// QualityItem::new(
/// mime::STAR_STAR,
/// q(800)
/// ),
/// ])
/// );
/// # }
/// ```
(Accept, http::ACCEPT) => (QualityItem<Mime>)+
test_accept {
// Tests from the RFC
test_header!(
test1,
vec![b"audio/*; q=0.2, audio/basic"],
Some(HeaderField(vec![
QualityItem::new("audio/*".parse().unwrap(), q(200)),
qitem("audio/basic".parse().unwrap()),
])));
test_header!(
test2,
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
Some(HeaderField(vec![
QualityItem::new(TEXT_PLAIN, q(500)),
qitem(TEXT_HTML),
QualityItem::new(
"text/x-dvi".parse().unwrap(),
q(800)),
qitem("text/x-c".parse().unwrap()),
])));
// Custom tests
test_header!(
test3,
vec![b"text/plain; charset=utf-8"],
Some(Accept(vec![
qitem(TEXT_PLAIN_UTF_8),
])));
test_header!(
test4,
vec![b"text/plain; charset=utf-8; q=0.5"],
Some(Accept(vec![
QualityItem::new(TEXT_PLAIN_UTF_8,
q(500)),
])));
#[test]
fn test_fuzzing1() {
use test::TestRequest;
let req = TestRequest::with_header(super::http::ACCEPT, "chunk#;e").finish();
let header = Accept::parse(&req);
assert!(header.is_ok());
}
}
}
impl Accept {
/// A constructor to easily create `Accept: */*`.
pub fn star() -> Accept {
Accept(vec![qitem(mime::STAR_STAR)])
}
/// A constructor to easily create `Accept: application/json`.
pub fn json() -> Accept {
Accept(vec![qitem(mime::APPLICATION_JSON)])
}
/// A constructor to easily create `Accept: text/*`.
pub fn text() -> Accept {
Accept(vec![qitem(mime::TEXT_STAR)])
}
/// A constructor to easily create `Accept: image/*`.
pub fn image() -> Accept {
Accept(vec![qitem(mime::IMAGE_STAR)])
}
}

View File

@ -0,0 +1,69 @@
use header::{Charset, QualityItem, ACCEPT_CHARSET};
header! {
/// `Accept-Charset` header, defined in
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3)
///
/// The `Accept-Charset` header field can be sent by a user agent to
/// indicate what charsets are acceptable in textual response content.
/// This field allows user agents capable of understanding more
/// comprehensive or special-purpose charsets to signal that capability
/// to an origin server that is capable of representing information in
/// those charsets.
///
/// # ABNF
///
/// ```text
/// Accept-Charset = 1#( ( charset / "*" ) [ weight ] )
/// ```
///
/// # Example values
/// * `iso-8859-5, unicode-1-1;q=0.8`
///
/// # Examples
/// ```rust
/// # extern crate actix_web;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptCharset, Charset, qitem};
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// AcceptCharset(vec![qitem(Charset::Us_Ascii)])
/// );
/// # }
/// ```
/// ```rust
/// # extern crate actix_web;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptCharset, Charset, q, QualityItem};
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// AcceptCharset(vec![
/// QualityItem::new(Charset::Us_Ascii, q(900)),
/// QualityItem::new(Charset::Iso_8859_10, q(200)),
/// ])
/// );
/// # }
/// ```
/// ```rust
/// # extern crate actix_web;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptCharset, Charset, qitem};
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))])
/// );
/// # }
/// ```
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+
test_accept_charset {
/// Test case from RFC
test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
}
}

View File

@ -0,0 +1,72 @@
use header::{Encoding, QualityItem};
header! {
/// `Accept-Encoding` header, defined in
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4)
///
/// The `Accept-Encoding` header field can be used by user agents to
/// indicate what response content-codings are
/// acceptable in the response. An `identity` token is used as a synonym
/// for "no encoding" in order to communicate when no encoding is
/// preferred.
///
/// # ABNF
///
/// ```text
/// Accept-Encoding = #( codings [ weight ] )
/// codings = content-coding / "identity" / "*"
/// ```
///
/// # Example values
/// * `compress, gzip`
/// * ``
/// * `*`
/// * `compress;q=0.5, gzip;q=1`
/// * `gzip;q=1.0, identity; q=0.5, *;q=0`
///
/// # Examples
/// ```
/// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem};
///
/// let mut headers = Headers::new();
/// headers.set(
/// AcceptEncoding(vec![qitem(Encoding::Chunked)])
/// );
/// ```
/// ```
/// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem};
///
/// let mut headers = Headers::new();
/// headers.set(
/// AcceptEncoding(vec![
/// qitem(Encoding::Chunked),
/// qitem(Encoding::Gzip),
/// qitem(Encoding::Deflate),
/// ])
/// );
/// ```
/// ```
/// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem};
///
/// let mut headers = Headers::new();
/// headers.set(
/// AcceptEncoding(vec![
/// qitem(Encoding::Chunked),
/// QualityItem::new(Encoding::Gzip, q(600)),
/// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)),
/// ])
/// );
/// ```
(AcceptEncoding, "Accept-Encoding") => (QualityItem<Encoding>)*
test_accept_encoding {
// From the RFC
test_header!(test1, vec![b"compress, gzip"]);
test_header!(test2, vec![b""], Some(AcceptEncoding(vec![])));
test_header!(test3, vec![b"*"]);
// Note: Removed quality 1 from gzip
test_header!(test4, vec![b"compress;q=0.5, gzip"]);
// Note: Removed quality 1 from gzip
test_header!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]);
}
}

View File

@ -0,0 +1,75 @@
use header::{QualityItem, ACCEPT_LANGUAGE};
use language_tags::LanguageTag;
header! {
/// `Accept-Language` header, defined in
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5)
///
/// The `Accept-Language` header field can be used by user agents to
/// indicate the set of natural languages that are preferred in the
/// response.
///
/// # ABNF
///
/// ```text
/// Accept-Language = 1#( language-range [ weight ] )
/// language-range = <language-range, see [RFC4647], Section 2.1>
/// ```
///
/// # Example values
/// * `da, en-gb;q=0.8, en;q=0.7`
/// * `en-us;q=1.0, en;q=0.5, fr`
///
/// # Examples
///
/// ```rust
/// # extern crate actix_web;
/// # extern crate language_tags;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptLanguage, LanguageTag, qitem};
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
/// let mut langtag: LanguageTag = Default::default();
/// langtag.language = Some("en".to_owned());
/// langtag.region = Some("US".to_owned());
/// builder.set(
/// AcceptLanguage(vec![
/// qitem(langtag),
/// ])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate actix_web;
/// # #[macro_use] extern crate language_tags;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem};
/// #
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// AcceptLanguage(vec![
/// qitem(langtag!(da)),
/// QualityItem::new(langtag!(en;;;GB), q(800)),
/// QualityItem::new(langtag!(en), q(700)),
/// ])
/// );
/// # }
/// ```
(AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem<LanguageTag>)+
test_accept_language {
// From the RFC
test_header!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]);
// Own test
test_header!(
test2, vec![b"en-US, en; q=0.5, fr"],
Some(AcceptLanguage(vec![
qitem("en-US".parse().unwrap()),
QualityItem::new("en".parse().unwrap(), q(500)),
qitem("fr".parse().unwrap()),
])));
}
}

View File

@ -0,0 +1,85 @@
use http::Method;
use http::header;
header! {
/// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1)
///
/// The `Allow` header field lists the set of methods advertised as
/// supported by the target resource. The purpose of this field is
/// strictly to inform the recipient of valid request methods associated
/// with the resource.
///
/// # ABNF
///
/// ```text
/// Allow = #method
/// ```
///
/// # Example values
/// * `GET, HEAD, PUT`
/// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr`
/// * ``
///
/// # Examples
///
/// ```rust
/// # extern crate http;
/// # extern crate actix_web;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::Allow;
/// use http::Method;
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// Allow(vec![Method::GET])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate http;
/// # extern crate actix_web;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::Allow;
/// use http::Method;
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// Allow(vec![
/// Method::GET,
/// Method::POST,
/// Method::PATCH,
/// ])
/// );
/// # }
/// ```
(Allow, header::ALLOW) => (Method)*
test_allow {
// From the RFC
test_header!(
test1,
vec![b"GET, HEAD, PUT"],
Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT])));
// Own tests
test_header!(
test2,
vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"],
Some(HeaderField(vec![
Method::OPTIONS,
Method::GET,
Method::PUT,
Method::POST,
Method::DELETE,
Method::HEAD,
Method::TRACE,
Method::CONNECT,
Method::PATCH])));
test_header!(
test3,
vec![b""],
Some(HeaderField(Vec::<Method>::new())));
}
}

View File

@ -0,0 +1,254 @@
use header::{Header, IntoHeaderValue, Writer};
use header::{fmt_comma_delimited, from_comma_delimited};
use http::header;
use std::fmt::{self, Write};
use std::str::FromStr;
/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2)
///
/// The `Cache-Control` header field is used to specify directives for
/// caches along the request/response chain. Such cache directives are
/// unidirectional in that the presence of a directive in a request does
/// not imply that the same directive is to be given in the response.
///
/// # ABNF
///
/// ```text
/// Cache-Control = 1#cache-directive
/// cache-directive = token [ "=" ( token / quoted-string ) ]
/// ```
///
/// # Example values
///
/// * `no-cache`
/// * `private, community="UCI"`
/// * `max-age=30`
///
/// # Examples
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{CacheControl, CacheDirective};
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
/// ```
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{CacheControl, CacheDirective};
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(CacheControl(vec![
/// CacheDirective::NoCache,
/// CacheDirective::Private,
/// CacheDirective::MaxAge(360u32),
/// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())),
/// ]));
/// ```
#[derive(PartialEq, Clone, Debug)]
pub struct CacheControl(pub Vec<CacheDirective>);
__hyper__deref!(CacheControl => Vec<CacheDirective>);
//TODO: this could just be the header! macro
impl Header for CacheControl {
fn name() -> header::HeaderName {
header::CACHE_CONTROL
}
#[inline]
fn parse<T>(msg: &T) -> Result<Self, ::error::ParseError>
where
T: ::HttpMessage,
{
let directives = from_comma_delimited(msg.headers().get_all(Self::name()))?;
if !directives.is_empty() {
Ok(CacheControl(directives))
} else {
Err(::error::ParseError::Header)
}
}
}
impl fmt::Display for CacheControl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt_comma_delimited(f, &self[..])
}
}
impl IntoHeaderValue for CacheControl {
type Error = header::InvalidHeaderValueBytes;
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_shared(writer.take())
}
}
/// `CacheControl` contains a list of these directives.
#[derive(PartialEq, Clone, Debug)]
pub enum CacheDirective {
/// "no-cache"
NoCache,
/// "no-store"
NoStore,
/// "no-transform"
NoTransform,
/// "only-if-cached"
OnlyIfCached,
// request directives
/// "max-age=delta"
MaxAge(u32),
/// "max-stale=delta"
MaxStale(u32),
/// "min-fresh=delta"
MinFresh(u32),
// response directives
/// "must-revalidate"
MustRevalidate,
/// "public"
Public,
/// "private"
Private,
/// "proxy-revalidate"
ProxyRevalidate,
/// "s-maxage=delta"
SMaxAge(u32),
/// Extension directives. Optionally include an argument.
Extension(String, Option<String>),
}
impl fmt::Display for CacheDirective {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::CacheDirective::*;
fmt::Display::fmt(
match *self {
NoCache => "no-cache",
NoStore => "no-store",
NoTransform => "no-transform",
OnlyIfCached => "only-if-cached",
MaxAge(secs) => return write!(f, "max-age={}", secs),
MaxStale(secs) => return write!(f, "max-stale={}", secs),
MinFresh(secs) => return write!(f, "min-fresh={}", secs),
MustRevalidate => "must-revalidate",
Public => "public",
Private => "private",
ProxyRevalidate => "proxy-revalidate",
SMaxAge(secs) => return write!(f, "s-maxage={}", secs),
Extension(ref name, None) => &name[..],
Extension(ref name, Some(ref arg)) => {
return write!(f, "{}={}", name, arg)
}
},
f,
)
}
}
impl FromStr for CacheDirective {
type Err = Option<<u32 as FromStr>::Err>;
fn from_str(s: &str) -> Result<CacheDirective, Option<<u32 as FromStr>::Err>> {
use self::CacheDirective::*;
match s {
"no-cache" => Ok(NoCache),
"no-store" => Ok(NoStore),
"no-transform" => Ok(NoTransform),
"only-if-cached" => Ok(OnlyIfCached),
"must-revalidate" => Ok(MustRevalidate),
"public" => Ok(Public),
"private" => Ok(Private),
"proxy-revalidate" => Ok(ProxyRevalidate),
"" => Err(None),
_ => match s.find('=') {
Some(idx) if idx + 1 < s.len() => {
match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) {
("max-age", secs) => secs.parse().map(MaxAge).map_err(Some),
("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some),
("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some),
("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some),
(left, right) => {
Ok(Extension(left.to_owned(), Some(right.to_owned())))
}
}
}
Some(_) => Err(None),
None => Ok(Extension(s.to_owned(), None)),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use header::Header;
use test::TestRequest;
#[test]
fn test_parse_multiple_headers() {
let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private")
.finish();
let cache = Header::parse(&req);
assert_eq!(
cache.ok(),
Some(CacheControl(vec![
CacheDirective::NoCache,
CacheDirective::Private,
]))
)
}
#[test]
fn test_parse_argument() {
let req =
TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private")
.finish();
let cache = Header::parse(&req);
assert_eq!(
cache.ok(),
Some(CacheControl(vec![
CacheDirective::MaxAge(100),
CacheDirective::Private,
]))
)
}
#[test]
fn test_parse_quote_form() {
let req =
TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish();
let cache = Header::parse(&req);
assert_eq!(
cache.ok(),
Some(CacheControl(vec![CacheDirective::MaxAge(200)]))
)
}
#[test]
fn test_parse_extension() {
let req =
TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish();
let cache = Header::parse(&req);
assert_eq!(
cache.ok(),
Some(CacheControl(vec![
CacheDirective::Extension("foo".to_owned(), None),
CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())),
]))
)
}
#[test]
fn test_parse_bad_syntax() {
let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish();
let cache: Result<CacheControl, _> = Header::parse(&req);
assert_eq!(cache.ok(), None)
}
}

View File

@ -0,0 +1,264 @@
// # References
//
// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc2388.txt
// Browser conformance tests at: http://greenbytes.de/tech/tc2231/
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
use language_tags::LanguageTag;
use std::fmt;
use unicase;
use header::{Header, Raw, parsing};
use header::parsing::{parse_extended_value, http_percent_encode};
use header::shared::Charset;
/// The implied disposition of the content of the HTTP body.
#[derive(Clone, Debug, PartialEq)]
pub enum DispositionType {
/// Inline implies default processing
Inline,
/// Attachment implies that the recipient should prompt the user to save the response locally,
/// rather than process it normally (as per its media type).
Attachment,
/// Extension type. Should be handled by recipients the same way as Attachment
Ext(String)
}
/// A parameter to the disposition type.
#[derive(Clone, Debug, PartialEq)]
pub enum DispositionParam {
/// A Filename consisting of a Charset, an optional LanguageTag, and finally a sequence of
/// bytes representing the filename
Filename(Charset, Option<LanguageTag>, Vec<u8>),
/// Extension type consisting of token and value. Recipients should ignore unrecognized
/// parameters.
Ext(String, String)
}
/// A `Content-Disposition` header, (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266).
///
/// The Content-Disposition response header field is used to convey
/// additional information about how to process the response payload, and
/// also can be used to attach additional metadata, such as the filename
/// to use when saving the response payload locally.
///
/// # ABNF
/// ```text
/// content-disposition = "Content-Disposition" ":"
/// disposition-type *( ";" disposition-parm )
///
/// disposition-type = "inline" | "attachment" | disp-ext-type
/// ; case-insensitive
///
/// disp-ext-type = token
///
/// disposition-parm = filename-parm | disp-ext-parm
///
/// filename-parm = "filename" "=" value
/// | "filename*" "=" ext-value
///
/// disp-ext-parm = token "=" value
/// | ext-token "=" ext-value
///
/// ext-token = <the characters in token, followed by "*">
/// ```
///
/// # Example
///
/// ```
/// use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset};
///
/// let mut headers = Headers::new();
/// headers.set(ContentDisposition {
/// disposition: DispositionType::Attachment,
/// parameters: vec![DispositionParam::Filename(
/// Charset::Iso_8859_1, // The character set for the bytes of the filename
/// None, // The optional language tag (see `language-tag` crate)
/// b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename
/// )]
/// });
/// ```
#[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition {
/// The disposition
pub disposition: DispositionType,
/// Disposition parameters
pub parameters: Vec<DispositionParam>,
}
impl Header for ContentDisposition {
fn header_name() -> &'static str {
static NAME: &'static str = "Content-Disposition";
NAME
}
fn parse_header(raw: &Raw) -> ::Result<ContentDisposition> {
parsing::from_one_raw_str(raw).and_then(|s: String| {
let mut sections = s.split(';');
let disposition = match sections.next() {
Some(s) => s.trim(),
None => return Err(::Error::Header),
};
let mut cd = ContentDisposition {
disposition: if unicase::eq_ascii(&*disposition, "inline") {
DispositionType::Inline
} else if unicase::eq_ascii(&*disposition, "attachment") {
DispositionType::Attachment
} else {
DispositionType::Ext(disposition.to_owned())
},
parameters: Vec::new(),
};
for section in sections {
let mut parts = section.splitn(2, '=');
let key = if let Some(key) = parts.next() {
key.trim()
} else {
return Err(::Error::Header);
};
let val = if let Some(val) = parts.next() {
val.trim()
} else {
return Err(::Error::Header);
};
cd.parameters.push(
if unicase::eq_ascii(&*key, "filename") {
DispositionParam::Filename(
Charset::Ext("UTF-8".to_owned()), None,
val.trim_matches('"').as_bytes().to_owned())
} else if unicase::eq_ascii(&*key, "filename*") {
let extended_value = try!(parse_extended_value(val));
DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value)
} else {
DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned())
}
);
}
Ok(cd)
})
}
#[inline]
fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result {
f.fmt_line(self)
}
}
impl fmt::Display for ContentDisposition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.disposition {
DispositionType::Inline => try!(write!(f, "inline")),
DispositionType::Attachment => try!(write!(f, "attachment")),
DispositionType::Ext(ref s) => try!(write!(f, "{}", s)),
}
for param in &self.parameters {
match *param {
DispositionParam::Filename(ref charset, ref opt_lang, ref bytes) => {
let mut use_simple_format: bool = false;
if opt_lang.is_none() {
if let Charset::Ext(ref ext) = *charset {
if unicase::eq_ascii(&**ext, "utf-8") {
use_simple_format = true;
}
}
}
if use_simple_format {
try!(write!(f, "; filename=\"{}\"",
match String::from_utf8(bytes.clone()) {
Ok(s) => s,
Err(_) => return Err(fmt::Error),
}));
} else {
try!(write!(f, "; filename*={}'", charset));
if let Some(ref lang) = *opt_lang {
try!(write!(f, "{}", lang));
};
try!(write!(f, "'"));
try!(http_percent_encode(f, bytes))
}
},
DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)),
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{ContentDisposition,DispositionType,DispositionParam};
use ::header::Header;
use ::header::shared::Charset;
#[test]
fn test_parse_header() {
assert!(ContentDisposition::parse_header(&"".into()).is_err());
let a = "form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Ext("form-data".to_owned()),
parameters: vec![
DispositionParam::Ext("dummy".to_owned(), "3".to_owned()),
DispositionParam::Ext("name".to_owned(), "upload".to_owned()),
DispositionParam::Filename(
Charset::Ext("UTF-8".to_owned()),
None,
"sample.png".bytes().collect()) ]
};
assert_eq!(a, b);
let a = "attachment; filename=\"image.jpg\"".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![
DispositionParam::Filename(
Charset::Ext("UTF-8".to_owned()),
None,
"image.jpg".bytes().collect()) ]
};
assert_eq!(a, b);
let a = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![
DispositionParam::Filename(
Charset::Ext("UTF-8".to_owned()),
None,
vec![0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20,
0xe2, 0x82, 0xac, 0x20, b'r', b'a', b't', b'e', b's']) ]
};
assert_eq!(a, b);
}
#[test]
fn test_display() {
let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
let a = as_string.into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let display_rendered = format!("{}",a);
assert_eq!(as_string, display_rendered);
let a = "attachment; filename*=UTF-8''black%20and%20white.csv".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let display_rendered = format!("{}",a);
assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered);
let a = "attachment; filename=colourful.csv".into();
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
let display_rendered = format!("{}",a);
assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
}
}

View File

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

View File

@ -0,0 +1,210 @@
use error::ParseError;
use header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer,
CONTENT_RANGE};
use std::fmt::{self, Display, Write};
use std::str::FromStr;
header! {
/// `Content-Range` header, defined in
/// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2)
(ContentRange, CONTENT_RANGE) => [ContentRangeSpec]
test_content_range {
test_header!(test_bytes,
vec![b"bytes 0-499/500"],
Some(ContentRange(ContentRangeSpec::Bytes {
range: Some((0, 499)),
instance_length: Some(500)
})));
test_header!(test_bytes_unknown_len,
vec![b"bytes 0-499/*"],
Some(ContentRange(ContentRangeSpec::Bytes {
range: Some((0, 499)),
instance_length: None
})));
test_header!(test_bytes_unknown_range,
vec![b"bytes */500"],
Some(ContentRange(ContentRangeSpec::Bytes {
range: None,
instance_length: Some(500)
})));
test_header!(test_unregistered,
vec![b"seconds 1-2"],
Some(ContentRange(ContentRangeSpec::Unregistered {
unit: "seconds".to_owned(),
resp: "1-2".to_owned()
})));
test_header!(test_no_len,
vec![b"bytes 0-499"],
None::<ContentRange>);
test_header!(test_only_unit,
vec![b"bytes"],
None::<ContentRange>);
test_header!(test_end_less_than_start,
vec![b"bytes 499-0/500"],
None::<ContentRange>);
test_header!(test_blank,
vec![b""],
None::<ContentRange>);
test_header!(test_bytes_many_spaces,
vec![b"bytes 1-2/500 3"],
None::<ContentRange>);
test_header!(test_bytes_many_slashes,
vec![b"bytes 1-2/500/600"],
None::<ContentRange>);
test_header!(test_bytes_many_dashes,
vec![b"bytes 1-2-3/500"],
None::<ContentRange>);
}
}
/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2)
///
/// # ABNF
///
/// ```text
/// Content-Range = byte-content-range
/// / other-content-range
///
/// byte-content-range = bytes-unit SP
/// ( byte-range-resp / unsatisfied-range )
///
/// byte-range-resp = byte-range "/" ( complete-length / "*" )
/// byte-range = first-byte-pos "-" last-byte-pos
/// unsatisfied-range = "*/" complete-length
///
/// complete-length = 1*DIGIT
///
/// other-content-range = other-range-unit SP other-range-resp
/// other-range-resp = *CHAR
/// ```
#[derive(PartialEq, Clone, Debug)]
pub enum ContentRangeSpec {
/// Byte range
Bytes {
/// First and last bytes of the range, omitted if request could not be
/// satisfied
range: Option<(u64, u64)>,
/// Total length of the instance, can be omitted if unknown
instance_length: Option<u64>,
},
/// Custom range, with unit not registered at IANA
Unregistered {
/// other-range-unit
unit: String,
/// other-range-resp
resp: String,
},
}
fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> {
let mut iter = s.splitn(2, separator);
match (iter.next(), iter.next()) {
(Some(a), Some(b)) => Some((a, b)),
_ => None,
}
}
impl FromStr for ContentRangeSpec {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, ParseError> {
let res = match split_in_two(s, ' ') {
Some(("bytes", resp)) => {
let (range, instance_length) =
split_in_two(resp, '/').ok_or(ParseError::Header)?;
let instance_length = if instance_length == "*" {
None
} else {
Some(instance_length
.parse()
.map_err(|_| ParseError::Header)?)
};
let range = if range == "*" {
None
} else {
let (first_byte, last_byte) =
split_in_two(range, '-').ok_or(ParseError::Header)?;
let first_byte = first_byte.parse().map_err(|_| ParseError::Header)?;
let last_byte = last_byte.parse().map_err(|_| ParseError::Header)?;
if last_byte < first_byte {
return Err(ParseError::Header);
}
Some((first_byte, last_byte))
};
ContentRangeSpec::Bytes {
range,
instance_length,
}
}
Some((unit, resp)) => ContentRangeSpec::Unregistered {
unit: unit.to_owned(),
resp: resp.to_owned(),
},
_ => return Err(ParseError::Header),
};
Ok(res)
}
}
impl Display for ContentRangeSpec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ContentRangeSpec::Bytes {
range,
instance_length,
} => {
f.write_str("bytes ")?;
match range {
Some((first_byte, last_byte)) => {
write!(f, "{}-{}", first_byte, last_byte)?;
}
None => {
f.write_str("*")?;
}
};
f.write_str("/")?;
if let Some(v) = instance_length {
write!(f, "{}", v)
} else {
f.write_str("*")
}
}
ContentRangeSpec::Unregistered {
ref unit,
ref resp,
} => {
f.write_str(unit)?;
f.write_str(" ")?;
f.write_str(resp)
}
}
}
}
impl IntoHeaderValue for ContentRangeSpec {
type Error = InvalidHeaderValueBytes;
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
HeaderValue::from_shared(writer.take())
}
}

View File

@ -0,0 +1,122 @@
use header::CONTENT_TYPE;
use mime::{self, Mime};
header! {
/// `Content-Type` header, defined in
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5)
///
/// The `Content-Type` header field indicates the media type of the
/// associated representation: either the representation enclosed in the
/// message payload or the selected representation, as determined by the
/// message semantics. The indicated media type defines both the data
/// format and how that data is intended to be processed by a recipient,
/// within the scope of the received message semantics, after any content
/// codings indicated by Content-Encoding are decoded.
///
/// Although the `mime` crate allows the mime options to be any slice, this crate
/// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If
/// this is an issue, it's possible to implement `Header` on a custom struct.
///
/// # ABNF
///
/// ```text
/// Content-Type = media-type
/// ```
///
/// # Example values
///
/// * `text/html; charset=utf-8`
/// * `application/json`
///
/// # Examples
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::ContentType;
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// ContentType::json()
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate mime;
/// # extern crate actix_web;
/// use mime::TEXT_HTML;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::ContentType;
///
/// # fn main() {
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// ContentType(TEXT_HTML)
/// );
/// # }
/// ```
(ContentType, CONTENT_TYPE) => [Mime]
test_content_type {
test_header!(
test1,
vec![b"text/html"],
Some(HeaderField(TEXT_HTML)));
}
}
impl ContentType {
/// A constructor to easily create a `Content-Type: application/json`
/// header.
#[inline]
pub fn json() -> ContentType {
ContentType(mime::APPLICATION_JSON)
}
/// A constructor to easily create a `Content-Type: text/plain;
/// charset=utf-8` header.
#[inline]
pub fn plaintext() -> ContentType {
ContentType(mime::TEXT_PLAIN_UTF_8)
}
/// A constructor to easily create a `Content-Type: text/html` header.
#[inline]
pub fn html() -> ContentType {
ContentType(mime::TEXT_HTML)
}
/// A constructor to easily create a `Content-Type: text/xml` header.
#[inline]
pub fn xml() -> ContentType {
ContentType(mime::TEXT_XML)
}
/// A constructor to easily create a `Content-Type:
/// application/www-form-url-encoded` header.
#[inline]
pub fn form_url_encoded() -> ContentType {
ContentType(mime::APPLICATION_WWW_FORM_URLENCODED)
}
/// A constructor to easily create a `Content-Type: image/jpeg` header.
#[inline]
pub fn jpeg() -> ContentType {
ContentType(mime::IMAGE_JPEG)
}
/// A constructor to easily create a `Content-Type: image/png` header.
#[inline]
pub fn png() -> ContentType {
ContentType(mime::IMAGE_PNG)
}
/// A constructor to easily create a `Content-Type:
/// application/octet-stream` header.
#[inline]
pub fn octet_stream() -> ContentType {
ContentType(mime::APPLICATION_OCTET_STREAM)
}
}
impl Eq for ContentType {}

41
src/header/common/date.rs Normal file
View File

@ -0,0 +1,41 @@
use header::{HttpDate, DATE};
use std::time::SystemTime;
header! {
/// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2)
///
/// The `Date` header field represents the date and time at which the
/// message was originated.
///
/// # ABNF
///
/// ```text
/// Date = HTTP-date
/// ```
///
/// # Example values
///
/// * `Tue, 15 Nov 1994 08:12:31 GMT`
///
/// # Example
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::Date;
/// use std::time::SystemTime;
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(Date(SystemTime::now().into()));
/// ```
(Date, DATE) => [HttpDate]
test_date {
test_header!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]);
}
}
impl Date {
pub fn now() -> Date {
Date(SystemTime::now().into())
}
}

96
src/header/common/etag.rs Normal file
View File

@ -0,0 +1,96 @@
use header::{EntityTag, ETAG};
header! {
/// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3)
///
/// The `ETag` header field in a response provides the current entity-tag
/// for the selected representation, as determined at the conclusion of
/// handling the request. An entity-tag is an opaque validator for
/// differentiating between multiple representations of the same
/// resource, regardless of whether those multiple representations are
/// due to resource state changes over time, content negotiation
/// resulting in multiple representations being valid at the same time,
/// or both. An entity-tag consists of an opaque quoted string, possibly
/// prefixed by a weakness indicator.
///
/// # ABNF
///
/// ```text
/// ETag = entity-tag
/// ```
///
/// # Example values
///
/// * `"xyzzy"`
/// * `W/"xyzzy"`
/// * `""`
///
/// # Examples
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{ETag, EntityTag};
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned())));
/// ```
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{ETag, EntityTag};
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned())));
/// ```
(ETag, ETAG) => [EntityTag]
test_etag {
// From the RFC
test_header!(test1,
vec![b"\"xyzzy\""],
Some(ETag(EntityTag::new(false, "xyzzy".to_owned()))));
test_header!(test2,
vec![b"W/\"xyzzy\""],
Some(ETag(EntityTag::new(true, "xyzzy".to_owned()))));
test_header!(test3,
vec![b"\"\""],
Some(ETag(EntityTag::new(false, "".to_owned()))));
// Own tests
test_header!(test4,
vec![b"\"foobar\""],
Some(ETag(EntityTag::new(false, "foobar".to_owned()))));
test_header!(test5,
vec![b"\"\""],
Some(ETag(EntityTag::new(false, "".to_owned()))));
test_header!(test6,
vec![b"W/\"weak-etag\""],
Some(ETag(EntityTag::new(true, "weak-etag".to_owned()))));
test_header!(test7,
vec![b"W/\"\x65\x62\""],
Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned()))));
test_header!(test8,
vec![b"W/\"\""],
Some(ETag(EntityTag::new(true, "".to_owned()))));
test_header!(test9,
vec![b"no-dquotes"],
None::<ETag>);
test_header!(test10,
vec![b"w/\"the-first-w-is-case-sensitive\""],
None::<ETag>);
test_header!(test11,
vec![b""],
None::<ETag>);
test_header!(test12,
vec![b"\"unmatched-dquotes1"],
None::<ETag>);
test_header!(test13,
vec![b"unmatched-dquotes2\""],
None::<ETag>);
test_header!(test14,
vec![b"matched-\"dquotes\""],
None::<ETag>);
test_header!(test15,
vec![b"\""],
None::<ETag>);
}
}

View File

@ -0,0 +1,39 @@
use header::{HttpDate, EXPIRES};
header! {
/// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3)
///
/// The `Expires` header field gives the date/time after which the
/// response is considered stale.
///
/// The presence of an Expires field does not imply that the original
/// resource will change or cease to exist at, before, or after that
/// time.
///
/// # ABNF
///
/// ```text
/// Expires = HTTP-date
/// ```
///
/// # Example values
/// * `Thu, 01 Dec 1994 16:00:00 GMT`
///
/// # Example
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::Expires;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = HttpResponse::Ok();
/// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
/// builder.set(Expires(expiration.into()));
/// ```
(Expires, EXPIRES) => [HttpDate]
test_expires {
// Test case from RFC
test_header!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]);
}
}

View File

@ -0,0 +1,70 @@
use header::{EntityTag, IF_MATCH};
header! {
/// `If-Match` header, defined in
/// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1)
///
/// The `If-Match` header field makes the request method conditional on
/// the recipient origin server either having at least one current
/// representation of the target resource, when the field-value is "*",
/// or having a current representation of the target resource that has an
/// entity-tag matching a member of the list of entity-tags provided in
/// the field-value.
///
/// An origin server MUST use the strong comparison function when
/// comparing entity-tags for `If-Match`, since the client
/// intends this precondition to prevent the method from being applied if
/// there have been any changes to the representation data.
///
/// # ABNF
///
/// ```text
/// If-Match = "*" / 1#entity-tag
/// ```
///
/// # Example values
///
/// * `"xyzzy"`
/// * "xyzzy", "r2d2xxxx", "c3piozzzz"
///
/// # Examples
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfMatch;
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(IfMatch::Any);
/// ```
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{IfMatch, EntityTag};
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// IfMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()),
/// EntityTag::new(false, "bazquux".to_owned()),
/// ])
/// );
/// ```
(IfMatch, IF_MATCH) => {Any / (EntityTag)+}
test_if_match {
test_header!(
test1,
vec![b"\"xyzzy\""],
Some(HeaderField::Items(
vec![EntityTag::new(false, "xyzzy".to_owned())])));
test_header!(
test2,
vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""],
Some(HeaderField::Items(
vec![EntityTag::new(false, "xyzzy".to_owned()),
EntityTag::new(false, "r2d2xxxx".to_owned()),
EntityTag::new(false, "c3piozzzz".to_owned())])));
test_header!(test3, vec![b"*"], Some(IfMatch::Any));
}
}

View File

@ -0,0 +1,39 @@
use header::{HttpDate, IF_MODIFIED_SINCE};
header! {
/// `If-Modified-Since` header, defined in
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3)
///
/// The `If-Modified-Since` header field makes a GET or HEAD request
/// method conditional on the selected representation's modification date
/// being more recent than the date provided in the field-value.
/// Transfer of the selected representation's data is avoided if that
/// data has not changed.
///
/// # ABNF
///
/// ```text
/// If-Unmodified-Since = HTTP-date
/// ```
///
/// # Example values
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
/// # Example
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfModifiedSince;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = HttpResponse::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.set(IfModifiedSince(modified.into()));
/// ```
(IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate]
test_if_modified_since {
// Test case from RFC
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
}
}

View File

@ -0,0 +1,92 @@
use header::{EntityTag, IF_NONE_MATCH};
header! {
/// `If-None-Match` header, defined in
/// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2)
///
/// The `If-None-Match` header field makes the request method conditional
/// on a recipient cache or origin server either not having any current
/// representation of the target resource, when the field-value is "*",
/// or having a selected representation with an entity-tag that does not
/// match any of those listed in the field-value.
///
/// A recipient MUST use the weak comparison function when comparing
/// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags
/// can be used for cache validation even if there have been changes to
/// the representation data.
///
/// # ABNF
///
/// ```text
/// If-None-Match = "*" / 1#entity-tag
/// ```
///
/// # Example values
///
/// * `"xyzzy"`
/// * `W/"xyzzy"`
/// * `"xyzzy", "r2d2xxxx", "c3piozzzz"`
/// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"`
/// * `*`
///
/// # Examples
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfNoneMatch;
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(IfNoneMatch::Any);
/// ```
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{IfNoneMatch, EntityTag};
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(
/// IfNoneMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()),
/// EntityTag::new(false, "bazquux".to_owned()),
/// ])
/// );
/// ```
(IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+}
test_if_none_match {
test_header!(test1, vec![b"\"xyzzy\""]);
test_header!(test2, vec![b"W/\"xyzzy\""]);
test_header!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]);
test_header!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]);
test_header!(test5, vec![b"*"]);
}
}
#[cfg(test)]
mod tests {
use super::IfNoneMatch;
use header::{EntityTag, Header, IF_NONE_MATCH};
use test::TestRequest;
#[test]
fn test_if_none_match() {
let mut if_none_match: Result<IfNoneMatch, _>;
let req = TestRequest::with_header(IF_NONE_MATCH, "*").finish();
if_none_match = Header::parse(&req);
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any));
let req =
TestRequest::with_header(IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..])
.finish();
if_none_match = Header::parse(&req);
let mut entities: Vec<EntityTag> = Vec::new();
let foobar_etag = EntityTag::new(false, "foobar".to_owned());
let weak_etag = EntityTag::new(true, "weak-etag".to_owned());
entities.push(foobar_etag);
entities.push(weak_etag);
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities)));
}
}

View File

@ -0,0 +1,115 @@
use error::ParseError;
use header::from_one_raw_str;
use header::{EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue,
InvalidHeaderValueBytes, Writer};
use http::header;
use httpmessage::HttpMessage;
use std::fmt::{self, Display, Write};
/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2)
///
/// If a client has a partial copy of a representation and wishes to have
/// an up-to-date copy of the entire representation, it could use the
/// Range header field with a conditional GET (using either or both of
/// If-Unmodified-Since and If-Match.) However, if the precondition
/// fails because the representation has been modified, the client would
/// then have to make a second request to obtain the entire current
/// representation.
///
/// The `If-Range` header field allows a client to \"short-circuit\" the
/// second request. Informally, its meaning is as follows: if the
/// representation is unchanged, send me the part(s) that I am requesting
/// in Range; otherwise, send me the entire representation.
///
/// # ABNF
///
/// ```text
/// If-Range = entity-tag / HTTP-date
/// ```
///
/// # Example values
///
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
/// * `\"xyzzy\"`
///
/// # Examples
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{EntityTag, IfRange};
///
/// let mut builder = HttpResponse::Ok();
/// builder.set(IfRange::EntityTag(EntityTag::new(
/// false,
/// "xyzzy".to_owned(),
/// )));
/// ```
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfRange;
/// use std::time::{Duration, SystemTime};
///
/// let mut builder = HttpResponse::Ok();
/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.set(IfRange::Date(fetched.into()));
/// ```
#[derive(Clone, Debug, PartialEq)]
pub enum IfRange {
/// The entity-tag the client has of the resource
EntityTag(EntityTag),
/// The date when the client retrieved the resource
Date(HttpDate),
}
impl Header for IfRange {
fn name() -> HeaderName {
header::IF_RANGE
}
#[inline]
fn parse<T>(msg: &T) -> Result<Self, ParseError>
where
T: HttpMessage,
{
let etag: Result<EntityTag, _> =
from_one_raw_str(msg.headers().get(header::IF_RANGE));
if let Ok(etag) = etag {
return Ok(IfRange::EntityTag(etag));
}
let date: Result<HttpDate, _> =
from_one_raw_str(msg.headers().get(header::IF_RANGE));
if let Ok(date) = date {
return Ok(IfRange::Date(date));
}
Err(ParseError::Header)
}
}
impl Display for IfRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
IfRange::EntityTag(ref x) => Display::fmt(x, f),
IfRange::Date(ref x) => Display::fmt(x, f),
}
}
}
impl IntoHeaderValue for IfRange {
type Error = InvalidHeaderValueBytes;
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
HeaderValue::from_shared(writer.take())
}
}
#[cfg(test)]
mod test_if_range {
use super::IfRange as HeaderField;
use header::*;
use std::str;
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
test_header!(test2, vec![b"\"xyzzy\""]);
test_header!(test3, vec![b"this-is-invalid"], None::<IfRange>);
}

View File

@ -0,0 +1,40 @@
use header::{HttpDate, IF_UNMODIFIED_SINCE};
header! {
/// `If-Unmodified-Since` header, defined in
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4)
///
/// The `If-Unmodified-Since` header field makes the request method
/// conditional on the selected representation's last modification date
/// being earlier than or equal to the date provided in the field-value.
/// This field accomplishes the same purpose as If-Match for cases where
/// the user agent does not have an entity-tag for the representation.
///
/// # ABNF
///
/// ```text
/// If-Unmodified-Since = HTTP-date
/// ```
///
/// # Example values
///
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
/// # Example
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfUnmodifiedSince;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = HttpResponse::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.set(IfUnmodifiedSince(modified.into()));
/// ```
(IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate]
test_if_unmodified_since {
// Test case from RFC
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
}
}

View File

@ -0,0 +1,38 @@
use header::{HttpDate, LAST_MODIFIED};
header! {
/// `Last-Modified` header, defined in
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2)
///
/// The `Last-Modified` header field in a response provides a timestamp
/// indicating the date and time at which the origin server believes the
/// selected representation was last modified, as determined at the
/// conclusion of handling the request.
///
/// # ABNF
///
/// ```text
/// Expires = HTTP-date
/// ```
///
/// # Example values
///
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
/// # Example
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web::http::header::LastModified;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = HttpResponse::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.set(LastModified(modified.into()));
/// ```
(LastModified, LAST_MODIFIED) => [HttpDate]
test_last_modified {
// Test case from RFC
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);}
}

352
src/header/common/mod.rs Normal file
View File

@ -0,0 +1,352 @@
//! A Collection of Header implementations for common HTTP Headers.
//!
//! ## Mime
//!
//! Several header fields use MIME values for their contents. Keeping with the
//! strongly-typed theme, the [mime](https://docs.rs/mime) crate
//! is used, such as `ContentType(pub Mime)`.
#![cfg_attr(rustfmt, rustfmt_skip)]
pub use self::accept_charset::AcceptCharset;
//pub use self::accept_encoding::AcceptEncoding;
pub use self::accept_language::AcceptLanguage;
pub use self::accept::Accept;
pub use self::allow::Allow;
pub use self::cache_control::{CacheControl, CacheDirective};
//pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam};
pub use self::content_language::ContentLanguage;
pub use self::content_range::{ContentRange, ContentRangeSpec};
pub use self::content_type::ContentType;
pub use self::date::Date;
pub use self::etag::ETag;
pub use self::expires::Expires;
pub use self::if_match::IfMatch;
pub use self::if_modified_since::IfModifiedSince;
pub use self::if_none_match::IfNoneMatch;
pub use self::if_range::IfRange;
pub use self::if_unmodified_since::IfUnmodifiedSince;
pub use self::last_modified::LastModified;
//pub use self::range::{Range, ByteRangeSpec};
#[doc(hidden)]
#[macro_export]
macro_rules! __hyper__deref {
($from:ty => $to:ty) => {
impl ::std::ops::Deref for $from {
type Target = $to;
#[inline]
fn deref(&self) -> &$to {
&self.0
}
}
impl ::std::ops::DerefMut for $from {
#[inline]
fn deref_mut(&mut self) -> &mut $to {
&mut self.0
}
}
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! __hyper__tm {
($id:ident, $tm:ident{$($tf:item)*}) => {
#[allow(unused_imports)]
#[cfg(test)]
mod $tm{
use std::str;
use http::Method;
use $crate::header::*;
use $crate::mime::*;
use super::$id as HeaderField;
$($tf)*
}
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! test_header {
($id:ident, $raw:expr) => {
#[test]
fn $id() {
#[allow(unused, deprecated)]
use std::ascii::AsciiExt;
use test;
let raw = $raw;
let a: Vec<Vec<u8>> = raw.iter().map(|x| x.to_vec()).collect();
let mut req = test::TestRequest::default();
for item in a {
req = req.header(HeaderField::name(), item);
}
let req = req.finish();
let value = HeaderField::parse(&req);
let result = format!("{}", value.unwrap());
let expected = String::from_utf8(raw[0].to_vec()).unwrap();
let result_cmp: Vec<String> = result
.to_ascii_lowercase()
.split(' ')
.map(|x| x.to_owned())
.collect();
let expected_cmp: Vec<String> = expected
.to_ascii_lowercase()
.split(' ')
.map(|x| x.to_owned())
.collect();
assert_eq!(result_cmp.concat(), expected_cmp.concat());
}
};
($id:ident, $raw:expr, $typed:expr) => {
#[test]
fn $id() {
use $crate::test;
let a: Vec<Vec<u8>> = $raw.iter().map(|x| x.to_vec()).collect();
let mut req = test::TestRequest::default();
for item in a {
req = req.header(HeaderField::name(), item);
}
let req = req.finish();
let val = HeaderField::parse(&req);
let typed: Option<HeaderField> = $typed;
// Test parsing
assert_eq!(val.ok(), typed);
// Test formatting
if typed.is_some() {
let raw = &($raw)[..];
let mut iter = raw.iter().map(|b|str::from_utf8(&b[..]).unwrap());
let mut joined = String::new();
joined.push_str(iter.next().unwrap());
for s in iter {
joined.push_str(", ");
joined.push_str(s);
}
assert_eq!(format!("{}", typed.unwrap()), joined);
}
}
}
}
#[macro_export]
macro_rules! header {
// $a:meta: Attributes associated with the header item (usually docs)
// $id:ident: Identifier of the header
// $n:expr: Lowercase name of the header
// $nn:expr: Nice name of the header
// List header, zero or more items
($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)*) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub struct $id(pub Vec<$item>);
__hyper__deref!($id => Vec<$item>);
impl $crate::header::Header for $id {
#[inline]
fn name() -> $crate::header::HeaderName {
$name
}
#[inline]
fn parse<T>(msg: &T) -> Result<Self, $crate::error::ParseError>
where T: $crate::HttpMessage
{
$crate::header::from_comma_delimited(
msg.headers().get_all(Self::name())).map($id)
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
$crate::header::fmt_comma_delimited(f, &self.0[..])
}
}
impl $crate::header::IntoHeaderValue for $id {
type Error = $crate::header::InvalidHeaderValueBytes;
fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> {
use std::fmt::Write;
let mut writer = $crate::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
$crate::header::HeaderValue::from_shared(writer.take())
}
}
};
// List header, one or more items
($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub struct $id(pub Vec<$item>);
__hyper__deref!($id => Vec<$item>);
impl $crate::header::Header for $id {
#[inline]
fn name() -> $crate::header::HeaderName {
$name
}
#[inline]
fn parse<T>(msg: &T) -> Result<Self, $crate::error::ParseError>
where T: $crate::HttpMessage
{
$crate::header::from_comma_delimited(
msg.headers().get_all(Self::name())).map($id)
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
$crate::header::fmt_comma_delimited(f, &self.0[..])
}
}
impl $crate::header::IntoHeaderValue for $id {
type Error = $crate::header::InvalidHeaderValueBytes;
fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> {
use std::fmt::Write;
let mut writer = $crate::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
$crate::header::HeaderValue::from_shared(writer.take())
}
}
};
// Single value header
($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub struct $id(pub $value);
__hyper__deref!($id => $value);
impl $crate::header::Header for $id {
#[inline]
fn name() -> $crate::header::HeaderName {
$name
}
#[inline]
fn parse<T>(msg: &T) -> Result<Self, $crate::error::ParseError>
where T: $crate::HttpMessage
{
$crate::header::from_one_raw_str(
msg.headers().get(Self::name())).map($id)
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::std::fmt::Display::fmt(&self.0, f)
}
}
impl $crate::header::IntoHeaderValue for $id {
type Error = $crate::header::InvalidHeaderValueBytes;
fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> {
self.0.try_into()
}
}
};
// List header, one or more items with "*" option
($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => {
$(#[$a])*
#[derive(Clone, Debug, PartialEq)]
pub enum $id {
/// Any value is a match
Any,
/// Only the listed items are a match
Items(Vec<$item>),
}
impl $crate::header::Header for $id {
#[inline]
fn name() -> $crate::header::HeaderName {
$name
}
#[inline]
fn parse<T>(msg: &T) -> Result<Self, $crate::error::ParseError>
where T: $crate::HttpMessage
{
let any = msg.headers().get(Self::name()).and_then(|hdr| {
hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))});
if let Some(true) = any {
Ok($id::Any)
} else {
Ok($id::Items(
$crate::header::from_comma_delimited(
msg.headers().get_all(Self::name()))?))
}
}
}
impl ::std::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
match *self {
$id::Any => f.write_str("*"),
$id::Items(ref fields) => $crate::header::fmt_comma_delimited(
f, &fields[..])
}
}
}
impl $crate::header::IntoHeaderValue for $id {
type Error = $crate::header::InvalidHeaderValueBytes;
fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> {
use std::fmt::Write;
let mut writer = $crate::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
$crate::header::HeaderValue::from_shared(writer.take())
}
}
};
// optional test module
($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])*
($id, $name) => ($item)*
}
__hyper__tm! { $id, $tm { $($tf)* }}
};
($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])*
($id, $n) => ($item)+
}
__hyper__tm! { $id, $tm { $($tf)* }}
};
($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])* ($id, $name) => [$item]
}
__hyper__tm! { $id, $tm { $($tf)* }}
};
($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => {
header! {
$(#[$a])*
($id, $name) => {Any / ($item)+}
}
__hyper__tm! { $id, $tm { $($tf)* }}
};
}
mod accept_charset;
//mod accept_encoding;
mod accept_language;
mod accept;
mod allow;
mod cache_control;
//mod content_disposition;
mod content_language;
mod content_range;
mod content_type;
mod date;
mod etag;
mod expires;
mod if_match;
mod if_modified_since;
mod if_none_match;
mod if_range;
mod if_unmodified_since;
mod last_modified;
//mod range;

434
src/header/common/range.rs Normal file
View File

@ -0,0 +1,434 @@
use std::fmt::{self, Display};
use std::str::FromStr;
use header::parsing::from_one_raw_str;
use header::{Header, Raw};
/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1)
///
/// The "Range" header field on a GET request modifies the method
/// semantics to request transfer of only one or more subranges of the
/// selected representation data, rather than the entire selected
/// representation data.
///
/// # ABNF
///
/// ```text
/// Range = byte-ranges-specifier / other-ranges-specifier
/// other-ranges-specifier = other-range-unit "=" other-range-set
/// other-range-set = 1*VCHAR
///
/// bytes-unit = "bytes"
///
/// byte-ranges-specifier = bytes-unit "=" byte-range-set
/// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec)
/// byte-range-spec = first-byte-pos "-" [last-byte-pos]
/// first-byte-pos = 1*DIGIT
/// last-byte-pos = 1*DIGIT
/// ```
///
/// # Example values
///
/// * `bytes=1000-`
/// * `bytes=-2000`
/// * `bytes=0-1,30-40`
/// * `bytes=0-10,20-90,-100`
/// * `custom_unit=0-123`
/// * `custom_unit=xxx-yyy`
///
/// # Examples
///
/// ```
/// use hyper::header::{Headers, Range, ByteRangeSpec};
///
/// let mut headers = Headers::new();
/// headers.set(Range::Bytes(
/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)]
/// ));
///
/// headers.clear();
/// headers.set(Range::Unregistered("letters".to_owned(), "a-f".to_owned()));
/// ```
///
/// ```
/// use hyper::header::{Headers, Range};
///
/// let mut headers = Headers::new();
/// headers.set(Range::bytes(1, 100));
///
/// headers.clear();
/// headers.set(Range::bytes_multi(vec![(1, 100), (200, 300)]));
/// ```
#[derive(PartialEq, Clone, Debug)]
pub enum Range {
/// Byte range
Bytes(Vec<ByteRangeSpec>),
/// Custom range, with unit not registered at IANA
/// (`other-range-unit`: String , `other-range-set`: String)
Unregistered(String, String),
}
/// Each `Range::Bytes` header can contain one or more `ByteRangeSpecs`.
/// Each `ByteRangeSpec` defines a range of bytes to fetch
#[derive(PartialEq, Clone, Debug)]
pub enum ByteRangeSpec {
/// Get all bytes between x and y ("x-y")
FromTo(u64, u64),
/// Get all bytes starting from x ("x-")
AllFrom(u64),
/// Get last x bytes ("-x")
Last(u64),
}
impl ByteRangeSpec {
/// Given the full length of the entity, attempt to normalize the byte range
/// into an satisfiable end-inclusive (from, to) range.
///
/// The resulting range is guaranteed to be a satisfiable range within the
/// bounds of `0 <= from <= to < full_length`.
///
/// If the byte range is deemed unsatisfiable, `None` is returned.
/// An unsatisfiable range is generally cause for a server to either reject
/// the client request with a `416 Range Not Satisfiable` status code, or to
/// simply ignore the range header and serve the full entity using a `200
/// OK` status code.
///
/// This function closely follows [RFC 7233][1] section 2.1.
/// As such, it considers ranges to be satisfiable if they meet the
/// following conditions:
///
/// > If a valid byte-range-set includes at least one byte-range-spec with
/// a first-byte-pos that is less than the current length of the
/// representation, or at least one suffix-byte-range-spec with a
/// non-zero suffix-length, then the byte-range-set is satisfiable.
/// Otherwise, the byte-range-set is unsatisfiable.
///
/// The function also computes remainder ranges based on the RFC:
///
/// > If the last-byte-pos value is
/// absent, or if the value is greater than or equal to the current
/// length of the representation data, the byte range is interpreted as
/// the remainder of the representation (i.e., the server replaces the
/// value of last-byte-pos with a value that is one less than the current
/// length of the selected representation).
///
/// [1]: https://tools.ietf.org/html/rfc7233
pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> {
// If the full length is zero, there is no satisfiable end-inclusive range.
if full_length == 0 {
return None;
}
match self {
&ByteRangeSpec::FromTo(from, to) => {
if from < full_length && from <= to {
Some((from, ::std::cmp::min(to, full_length - 1)))
} else {
None
}
}
&ByteRangeSpec::AllFrom(from) => {
if from < full_length {
Some((from, full_length - 1))
} else {
None
}
}
&ByteRangeSpec::Last(last) => {
if last > 0 {
// From the RFC: If the selected representation is shorter
// than the specified suffix-length,
// the entire representation is used.
if last > full_length {
Some((0, full_length - 1))
} else {
Some((full_length - last, full_length - 1))
}
} else {
None
}
}
}
}
}
impl Range {
/// Get the most common byte range header ("bytes=from-to")
pub fn bytes(from: u64, to: u64) -> Range {
Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)])
}
/// Get byte range header with multiple subranges
/// ("bytes=from1-to1,from2-to2,fromX-toX")
pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range {
Range::Bytes(
ranges
.iter()
.map(|r| ByteRangeSpec::FromTo(r.0, r.1))
.collect(),
)
}
}
impl fmt::Display for ByteRangeSpec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to),
ByteRangeSpec::Last(pos) => write!(f, "-{}", pos),
ByteRangeSpec::AllFrom(pos) => write!(f, "{}-", pos),
}
}
}
impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Range::Bytes(ref ranges) => {
try!(write!(f, "bytes="));
for (i, range) in ranges.iter().enumerate() {
if i != 0 {
try!(f.write_str(","));
}
try!(Display::fmt(range, f));
}
Ok(())
}
Range::Unregistered(ref unit, ref range_str) => {
write!(f, "{}={}", unit, range_str)
}
}
}
}
impl FromStr for Range {
type Err = ::Error;
fn from_str(s: &str) -> ::Result<Range> {
let mut iter = s.splitn(2, '=');
match (iter.next(), iter.next()) {
(Some("bytes"), Some(ranges)) => {
let ranges = from_comma_delimited(ranges);
if ranges.is_empty() {
return Err(::Error::Header);
}
Ok(Range::Bytes(ranges))
}
(Some(unit), Some(range_str)) if unit != "" && range_str != "" => Ok(
Range::Unregistered(unit.to_owned(), range_str.to_owned()),
),
_ => Err(::Error::Header),
}
}
}
impl FromStr for ByteRangeSpec {
type Err = ::Error;
fn from_str(s: &str) -> ::Result<ByteRangeSpec> {
let mut parts = s.splitn(2, '-');
match (parts.next(), parts.next()) {
(Some(""), Some(end)) => end.parse()
.or(Err(::Error::Header))
.map(ByteRangeSpec::Last),
(Some(start), Some("")) => start
.parse()
.or(Err(::Error::Header))
.map(ByteRangeSpec::AllFrom),
(Some(start), Some(end)) => match (start.parse(), end.parse()) {
(Ok(start), Ok(end)) if start <= end => {
Ok(ByteRangeSpec::FromTo(start, end))
}
_ => Err(::Error::Header),
},
_ => Err(::Error::Header),
}
}
}
fn from_comma_delimited<T: FromStr>(s: &str) -> Vec<T> {
s.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y),
})
.filter_map(|x| x.parse().ok())
.collect()
}
impl Header for Range {
fn header_name() -> &'static str {
static NAME: &'static str = "Range";
NAME
}
fn parse_header(raw: &Raw) -> ::Result<Range> {
from_one_raw_str(raw)
}
fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result {
f.fmt_line(self)
}
}
#[test]
fn test_parse_bytes_range_valid() {
let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap();
let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap();
let r3 = Range::bytes(1, 100);
assert_eq!(r, r2);
assert_eq!(r2, r3);
let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap();
let r2: Range =
Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap();
let r3 = Range::Bytes(vec![
ByteRangeSpec::FromTo(1, 100),
ByteRangeSpec::AllFrom(200),
]);
assert_eq!(r, r2);
assert_eq!(r2, r3);
let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap();
let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap();
let r3 = Range::Bytes(vec![
ByteRangeSpec::FromTo(1, 100),
ByteRangeSpec::Last(100),
]);
assert_eq!(r, r2);
assert_eq!(r2, r3);
let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap();
let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned());
assert_eq!(r, r2);
}
#[test]
fn test_parse_unregistered_range_valid() {
let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap();
let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned());
assert_eq!(r, r2);
let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap();
let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned());
assert_eq!(r, r2);
let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap();
let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned());
assert_eq!(r, r2);
}
#[test]
fn test_parse_invalid() {
let r: ::Result<Range> = Header::parse_header(&"bytes=1-a,-".into());
assert_eq!(r.ok(), None);
let r: ::Result<Range> = Header::parse_header(&"bytes=1-2-3".into());
assert_eq!(r.ok(), None);
let r: ::Result<Range> = Header::parse_header(&"abc".into());
assert_eq!(r.ok(), None);
let r: ::Result<Range> = Header::parse_header(&"bytes=1-100=".into());
assert_eq!(r.ok(), None);
let r: ::Result<Range> = Header::parse_header(&"bytes=".into());
assert_eq!(r.ok(), None);
let r: ::Result<Range> = Header::parse_header(&"custom=".into());
assert_eq!(r.ok(), None);
let r: ::Result<Range> = Header::parse_header(&"=1-100".into());
assert_eq!(r.ok(), None);
}
#[test]
fn test_fmt() {
use header::Headers;
let mut headers = Headers::new();
headers.set(Range::Bytes(vec![
ByteRangeSpec::FromTo(0, 1000),
ByteRangeSpec::AllFrom(2000),
]));
assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n");
headers.clear();
headers.set(Range::Bytes(vec![]));
assert_eq!(&headers.to_string(), "Range: bytes=\r\n");
headers.clear();
headers.set(Range::Unregistered(
"custom".to_owned(),
"1-xxx".to_owned(),
));
assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n");
}
#[test]
fn test_byte_range_spec_to_satisfiable_range() {
assert_eq!(
Some((0, 0)),
ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3)
);
assert_eq!(
Some((1, 2)),
ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3)
);
assert_eq!(
Some((1, 2)),
ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3)
);
assert_eq!(
None,
ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3)
);
assert_eq!(
None,
ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3)
);
assert_eq!(
None,
ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0)
);
assert_eq!(
Some((0, 2)),
ByteRangeSpec::AllFrom(0).to_satisfiable_range(3)
);
assert_eq!(
Some((2, 2)),
ByteRangeSpec::AllFrom(2).to_satisfiable_range(3)
);
assert_eq!(
None,
ByteRangeSpec::AllFrom(3).to_satisfiable_range(3)
);
assert_eq!(
None,
ByteRangeSpec::AllFrom(5).to_satisfiable_range(3)
);
assert_eq!(
None,
ByteRangeSpec::AllFrom(0).to_satisfiable_range(0)
);
assert_eq!(
Some((1, 2)),
ByteRangeSpec::Last(2).to_satisfiable_range(3)
);
assert_eq!(
Some((2, 2)),
ByteRangeSpec::Last(1).to_satisfiable_range(3)
);
assert_eq!(
Some((0, 2)),
ByteRangeSpec::Last(5).to_satisfiable_range(3)
);
assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3));
assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0));
}

259
src/header/mod.rs Normal file
View File

@ -0,0 +1,259 @@
//! Various http headers
// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header)
use std::fmt;
use std::str::FromStr;
use bytes::{Bytes, BytesMut};
use mime::Mime;
use modhttp::header::GetAll;
use modhttp::Error as HttpError;
pub use modhttp::header::*;
use error::ParseError;
use httpmessage::HttpMessage;
mod common;
mod shared;
#[doc(hidden)]
pub use self::common::*;
#[doc(hidden)]
pub use self::shared::*;
#[doc(hidden)]
/// A trait for any object that will represent a header field and value.
pub trait Header
where
Self: IntoHeaderValue,
{
/// Returns the name of the header field
fn name() -> HeaderName;
/// Parse a header
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
}
#[doc(hidden)]
/// A trait for any object that can be Converted to a `HeaderValue`
pub trait IntoHeaderValue: Sized {
/// The type returned in the event of a conversion error.
type Error: Into<HttpError>;
/// Cast from PyObject to a concrete Python object type.
fn try_into(self) -> Result<HeaderValue, Self::Error>;
}
impl IntoHeaderValue for HeaderValue {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
Ok(self)
}
}
impl<'a> IntoHeaderValue for &'a str {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
self.parse()
}
}
impl<'a> IntoHeaderValue for &'a [u8] {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_bytes(self)
}
}
impl IntoHeaderValue for Bytes {
type Error = InvalidHeaderValueBytes;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_shared(self)
}
}
impl IntoHeaderValue for Vec<u8> {
type Error = InvalidHeaderValueBytes;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_shared(Bytes::from(self))
}
}
impl IntoHeaderValue for String {
type Error = InvalidHeaderValueBytes;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_shared(Bytes::from(self))
}
}
impl IntoHeaderValue for Mime {
type Error = InvalidHeaderValueBytes;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_shared(Bytes::from(format!("{}", self)))
}
}
/// Represents supported types of content encodings
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ContentEncoding {
/// Automatically select encoding based on encoding negotiation
Auto,
/// A format using the Brotli algorithm
#[cfg(feature = "brotli")]
Br,
/// A format using the zlib structure with deflate algorithm
#[cfg(feature = "flate2")]
Deflate,
/// Gzip algorithm
#[cfg(feature = "flate2")]
Gzip,
/// Indicates the identity function (i.e. no compression, nor modification)
Identity,
}
impl ContentEncoding {
#[inline]
pub fn is_compression(&self) -> bool {
match *self {
ContentEncoding::Identity | ContentEncoding::Auto => false,
_ => true,
}
}
#[inline]
pub fn as_str(&self) -> &'static str {
match *self {
#[cfg(feature = "brotli")]
ContentEncoding::Br => "br",
#[cfg(feature = "flate2")]
ContentEncoding::Gzip => "gzip",
#[cfg(feature = "flate2")]
ContentEncoding::Deflate => "deflate",
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
}
}
#[inline]
/// default quality value
pub fn quality(&self) -> f64 {
match *self {
#[cfg(feature = "brotli")]
ContentEncoding::Br => 1.1,
#[cfg(feature = "flate2")]
ContentEncoding::Gzip => 1.0,
#[cfg(feature = "flate2")]
ContentEncoding::Deflate => 0.9,
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
}
}
}
// TODO: remove memory allocation
impl<'a> From<&'a str> for ContentEncoding {
fn from(s: &'a str) -> ContentEncoding {
match AsRef::<str>::as_ref(&s.trim().to_lowercase()) {
#[cfg(feature = "brotli")]
"br" => ContentEncoding::Br,
#[cfg(feature = "flate2")]
"gzip" => ContentEncoding::Gzip,
#[cfg(feature = "flate2")]
"deflate" => ContentEncoding::Deflate,
_ => ContentEncoding::Identity,
}
}
}
#[doc(hidden)]
pub(crate) struct Writer {
buf: BytesMut,
}
impl Writer {
fn new() -> Writer {
Writer {
buf: BytesMut::new(),
}
}
fn take(&mut self) -> Bytes {
self.buf.take().freeze()
}
}
impl fmt::Write for Writer {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.buf.extend_from_slice(s.as_bytes());
Ok(())
}
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result {
fmt::write(self, args)
}
}
#[inline]
#[doc(hidden)]
/// Reads a comma-delimited raw header into a Vec.
pub fn from_comma_delimited<T: FromStr>(
all: GetAll<HeaderValue>,
) -> Result<Vec<T>, ParseError> {
let mut result = Vec::new();
for h in all {
let s = h.to_str().map_err(|_| ParseError::Header)?;
result.extend(
s.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y),
})
.filter_map(|x| x.trim().parse().ok()),
)
}
Ok(result)
}
#[inline]
#[doc(hidden)]
/// Reads a single string when parsing a header.
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
if let Some(line) = val {
let line = line.to_str().map_err(|_| ParseError::Header)?;
if !line.is_empty() {
return T::from_str(line).or(Err(ParseError::Header));
}
}
Err(ParseError::Header)
}
#[inline]
#[doc(hidden)]
/// Format an array into a comma-delimited string.
pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result
where
T: fmt::Display,
{
let mut iter = parts.iter();
if let Some(part) = iter.next() {
fmt::Display::fmt(part, f)?;
}
for part in iter {
f.write_str(", ")?;
fmt::Display::fmt(part, f)?;
}
Ok(())
}

View File

@ -0,0 +1,154 @@
#![allow(unused, deprecated)]
use std::ascii::AsciiExt;
use std::fmt::{self, Display};
use std::str::FromStr;
use self::Charset::*;
/// A Mime charset.
///
/// The string representation is normalized to upper case.
///
/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url].
///
/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml
#[derive(Clone, Debug, PartialEq)]
#[allow(non_camel_case_types)]
pub enum Charset {
/// US ASCII
Us_Ascii,
/// ISO-8859-1
Iso_8859_1,
/// ISO-8859-2
Iso_8859_2,
/// ISO-8859-3
Iso_8859_3,
/// ISO-8859-4
Iso_8859_4,
/// ISO-8859-5
Iso_8859_5,
/// ISO-8859-6
Iso_8859_6,
/// ISO-8859-7
Iso_8859_7,
/// ISO-8859-8
Iso_8859_8,
/// ISO-8859-9
Iso_8859_9,
/// ISO-8859-10
Iso_8859_10,
/// Shift_JIS
Shift_Jis,
/// EUC-JP
Euc_Jp,
/// ISO-2022-KR
Iso_2022_Kr,
/// EUC-KR
Euc_Kr,
/// ISO-2022-JP
Iso_2022_Jp,
/// ISO-2022-JP-2
Iso_2022_Jp_2,
/// ISO-8859-6-E
Iso_8859_6_E,
/// ISO-8859-6-I
Iso_8859_6_I,
/// ISO-8859-8-E
Iso_8859_8_E,
/// ISO-8859-8-I
Iso_8859_8_I,
/// GB2312
Gb2312,
/// Big5
Big5,
/// KOI8-R
Koi8_R,
/// An arbitrary charset specified as a string
Ext(String),
}
impl Charset {
fn name(&self) -> &str {
match *self {
Us_Ascii => "US-ASCII",
Iso_8859_1 => "ISO-8859-1",
Iso_8859_2 => "ISO-8859-2",
Iso_8859_3 => "ISO-8859-3",
Iso_8859_4 => "ISO-8859-4",
Iso_8859_5 => "ISO-8859-5",
Iso_8859_6 => "ISO-8859-6",
Iso_8859_7 => "ISO-8859-7",
Iso_8859_8 => "ISO-8859-8",
Iso_8859_9 => "ISO-8859-9",
Iso_8859_10 => "ISO-8859-10",
Shift_Jis => "Shift-JIS",
Euc_Jp => "EUC-JP",
Iso_2022_Kr => "ISO-2022-KR",
Euc_Kr => "EUC-KR",
Iso_2022_Jp => "ISO-2022-JP",
Iso_2022_Jp_2 => "ISO-2022-JP-2",
Iso_8859_6_E => "ISO-8859-6-E",
Iso_8859_6_I => "ISO-8859-6-I",
Iso_8859_8_E => "ISO-8859-8-E",
Iso_8859_8_I => "ISO-8859-8-I",
Gb2312 => "GB2312",
Big5 => "5",
Koi8_R => "KOI8-R",
Ext(ref s) => s,
}
}
}
impl Display for Charset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.name())
}
}
impl FromStr for Charset {
type Err = ::Error;
fn from_str(s: &str) -> ::Result<Charset> {
Ok(match s.to_ascii_uppercase().as_ref() {
"US-ASCII" => Us_Ascii,
"ISO-8859-1" => Iso_8859_1,
"ISO-8859-2" => Iso_8859_2,
"ISO-8859-3" => Iso_8859_3,
"ISO-8859-4" => Iso_8859_4,
"ISO-8859-5" => Iso_8859_5,
"ISO-8859-6" => Iso_8859_6,
"ISO-8859-7" => Iso_8859_7,
"ISO-8859-8" => Iso_8859_8,
"ISO-8859-9" => Iso_8859_9,
"ISO-8859-10" => Iso_8859_10,
"SHIFT-JIS" => Shift_Jis,
"EUC-JP" => Euc_Jp,
"ISO-2022-KR" => Iso_2022_Kr,
"EUC-KR" => Euc_Kr,
"ISO-2022-JP" => Iso_2022_Jp,
"ISO-2022-JP-2" => Iso_2022_Jp_2,
"ISO-8859-6-E" => Iso_8859_6_E,
"ISO-8859-6-I" => Iso_8859_6_I,
"ISO-8859-8-E" => Iso_8859_8_E,
"ISO-8859-8-I" => Iso_8859_8_I,
"GB2312" => Gb2312,
"5" => Big5,
"KOI8-R" => Koi8_R,
s => Ext(s.to_owned()),
})
}
}
#[test]
fn test_parse() {
assert_eq!(Us_Ascii, "us-ascii".parse().unwrap());
assert_eq!(Us_Ascii, "US-Ascii".parse().unwrap());
assert_eq!(Us_Ascii, "US-ASCII".parse().unwrap());
assert_eq!(Shift_Jis, "Shift-JIS".parse().unwrap());
assert_eq!(Ext("ABCD".to_owned()), "abcd".parse().unwrap());
}
#[test]
fn test_display() {
assert_eq!("US-ASCII", format!("{}", Us_Ascii));
assert_eq!("ABCD", format!("{}", Ext("ABCD".to_owned())));
}

View File

@ -0,0 +1,59 @@
use std::fmt;
use std::str;
pub use self::Encoding::{
Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers,
};
/// A value to represent an encoding used in `Transfer-Encoding`
/// or `Accept-Encoding` header.
#[derive(Clone, PartialEq, Debug)]
pub enum Encoding {
/// The `chunked` encoding.
Chunked,
/// The `br` encoding.
Brotli,
/// The `gzip` encoding.
Gzip,
/// The `deflate` encoding.
Deflate,
/// The `compress` encoding.
Compress,
/// The `identity` encoding.
Identity,
/// The `trailers` encoding.
Trailers,
/// Some other encoding that is less common, can be any String.
EncodingExt(String),
}
impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match *self {
Chunked => "chunked",
Brotli => "br",
Gzip => "gzip",
Deflate => "deflate",
Compress => "compress",
Identity => "identity",
Trailers => "trailers",
EncodingExt(ref s) => s.as_ref(),
})
}
}
impl str::FromStr for Encoding {
type Err = ::error::ParseError;
fn from_str(s: &str) -> Result<Encoding, ::error::ParseError> {
match s {
"chunked" => Ok(Chunked),
"br" => Ok(Brotli),
"deflate" => Ok(Deflate),
"gzip" => Ok(Gzip),
"compress" => Ok(Compress),
"identity" => Ok(Identity),
"trailers" => Ok(Trailers),
_ => Ok(EncodingExt(s.to_owned())),
}
}
}

266
src/header/shared/entity.rs Normal file
View File

@ -0,0 +1,266 @@
use header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer};
use std::fmt::{self, Display, Write};
use std::str::FromStr;
/// check that each char in the slice is either:
/// 1. `%x21`, or
/// 2. in the range `%x23` to `%x7E`, or
/// 3. above `%x80`
fn check_slice_validity(slice: &str) -> bool {
slice
.bytes()
.all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80'))
}
/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3)
///
/// An entity tag consists of a string enclosed by two literal double quotes.
/// Preceding the first double quote is an optional weakness indicator,
/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and
/// `W/"xyzzy"`.
///
/// # ABNF
///
/// ```text
/// entity-tag = [ weak ] opaque-tag
/// weak = %x57.2F ; "W/", case-sensitive
/// opaque-tag = DQUOTE *etagc DQUOTE
/// etagc = %x21 / %x23-7E / obs-text
/// ; VCHAR except double quotes, plus obs-text
/// ```
///
/// # Comparison
/// To check if two entity tags are equivalent in an application always use the
/// `strong_eq` or `weak_eq` methods based on the context of the Tag. Only use
/// `==` to check if two tags are identical.
///
/// The example below shows the results for a set of entity-tag pairs and
/// both the weak and strong comparison function results:
///
/// | `ETag 1`| `ETag 2`| Strong Comparison | Weak Comparison |
/// |---------|---------|-------------------|-----------------|
/// | `W/"1"` | `W/"1"` | no match | match |
/// | `W/"1"` | `W/"2"` | no match | no match |
/// | `W/"1"` | `"1"` | no match | match |
/// | `"1"` | `"1"` | match | match |
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntityTag {
/// Weakness indicator for the tag
pub weak: bool,
/// The opaque string in between the DQUOTEs
tag: String,
}
impl EntityTag {
/// Constructs a new EntityTag.
/// # Panics
/// If the tag contains invalid characters.
pub fn new(weak: bool, tag: String) -> EntityTag {
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
EntityTag { weak, tag }
}
/// Constructs a new weak EntityTag.
/// # Panics
/// If the tag contains invalid characters.
pub fn weak(tag: String) -> EntityTag {
EntityTag::new(true, tag)
}
/// Constructs a new strong EntityTag.
/// # Panics
/// If the tag contains invalid characters.
pub fn strong(tag: String) -> EntityTag {
EntityTag::new(false, tag)
}
/// Get the tag.
pub fn tag(&self) -> &str {
self.tag.as_ref()
}
/// Set the tag.
/// # Panics
/// If the tag contains invalid characters.
pub fn set_tag(&mut self, tag: String) {
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
self.tag = tag
}
/// For strong comparison two entity-tags are equivalent if both are not
/// weak and their opaque-tags match character-by-character.
pub fn strong_eq(&self, other: &EntityTag) -> bool {
!self.weak && !other.weak && self.tag == other.tag
}
/// For weak comparison two entity-tags are equivalent if their
/// opaque-tags match character-by-character, regardless of either or
/// both being tagged as "weak".
pub fn weak_eq(&self, other: &EntityTag) -> bool {
self.tag == other.tag
}
/// The inverse of `EntityTag.strong_eq()`.
pub fn strong_ne(&self, other: &EntityTag) -> bool {
!self.strong_eq(other)
}
/// The inverse of `EntityTag.weak_eq()`.
pub fn weak_ne(&self, other: &EntityTag) -> bool {
!self.weak_eq(other)
}
}
impl Display for EntityTag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.weak {
write!(f, "W/\"{}\"", self.tag)
} else {
write!(f, "\"{}\"", self.tag)
}
}
}
impl FromStr for EntityTag {
type Err = ::error::ParseError;
fn from_str(s: &str) -> Result<EntityTag, ::error::ParseError> {
let length: usize = s.len();
let slice = &s[..];
// Early exits if it doesn't terminate in a DQUOTE.
if !slice.ends_with('"') || slice.len() < 2 {
return Err(::error::ParseError::Header);
}
// The etag is weak if its first char is not a DQUOTE.
if slice.len() >= 2
&& slice.starts_with('"')
&& check_slice_validity(&slice[1..length - 1])
{
// No need to check if the last char is a DQUOTE,
// we already did that above.
return Ok(EntityTag {
weak: false,
tag: slice[1..length - 1].to_owned(),
});
} else if slice.len() >= 4
&& slice.starts_with("W/\"")
&& check_slice_validity(&slice[3..length - 1])
{
return Ok(EntityTag {
weak: true,
tag: slice[3..length - 1].to_owned(),
});
}
Err(::error::ParseError::Header)
}
}
impl IntoHeaderValue for EntityTag {
type Error = InvalidHeaderValueBytes;
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = Writer::new();
write!(wrt, "{}", self).unwrap();
unsafe { Ok(HeaderValue::from_shared_unchecked(wrt.take())) }
}
}
#[cfg(test)]
mod tests {
use super::EntityTag;
#[test]
fn test_etag_parse_success() {
// Expected success
assert_eq!(
"\"foobar\"".parse::<EntityTag>().unwrap(),
EntityTag::strong("foobar".to_owned())
);
assert_eq!(
"\"\"".parse::<EntityTag>().unwrap(),
EntityTag::strong("".to_owned())
);
assert_eq!(
"W/\"weaktag\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("weaktag".to_owned())
);
assert_eq!(
"W/\"\x65\x62\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("\x65\x62".to_owned())
);
assert_eq!(
"W/\"\"".parse::<EntityTag>().unwrap(),
EntityTag::weak("".to_owned())
);
}
#[test]
fn test_etag_parse_failures() {
// Expected failures
assert!("no-dquotes".parse::<EntityTag>().is_err());
assert!(
"w/\"the-first-w-is-case-sensitive\""
.parse::<EntityTag>()
.is_err()
);
assert!("".parse::<EntityTag>().is_err());
assert!("\"unmatched-dquotes1".parse::<EntityTag>().is_err());
assert!("unmatched-dquotes2\"".parse::<EntityTag>().is_err());
assert!("matched-\"dquotes\"".parse::<EntityTag>().is_err());
}
#[test]
fn test_etag_fmt() {
assert_eq!(
format!("{}", EntityTag::strong("foobar".to_owned())),
"\"foobar\""
);
assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\"");
assert_eq!(
format!("{}", EntityTag::weak("weak-etag".to_owned())),
"W/\"weak-etag\""
);
assert_eq!(
format!("{}", EntityTag::weak("\u{0065}".to_owned())),
"W/\"\x65\""
);
assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\"");
}
#[test]
fn test_cmp() {
// | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison |
// |---------|---------|-------------------|-----------------|
// | `W/"1"` | `W/"1"` | no match | match |
// | `W/"1"` | `W/"2"` | no match | no match |
// | `W/"1"` | `"1"` | no match | match |
// | `"1"` | `"1"` | match | match |
let mut etag1 = EntityTag::weak("1".to_owned());
let mut etag2 = EntityTag::weak("1".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::weak("1".to_owned());
etag2 = EntityTag::weak("2".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(!etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(etag1.weak_ne(&etag2));
etag1 = EntityTag::weak("1".to_owned());
etag2 = EntityTag::strong("1".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::strong("1".to_owned());
etag2 = EntityTag::strong("1".to_owned());
assert!(etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(!etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
}
}

View File

@ -0,0 +1,123 @@
use std::fmt::{self, Display};
use std::io::Write;
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use bytes::{BufMut, BytesMut};
use http::header::{HeaderValue, InvalidHeaderValueBytes};
use time;
use error::ParseError;
use header::IntoHeaderValue;
/// A timestamp with HTTP formatting and parsing
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpDate(time::Tm);
impl FromStr for HttpDate {
type Err = ParseError;
fn from_str(s: &str) -> Result<HttpDate, ParseError> {
match time::strptime(s, "%a, %d %b %Y %T %Z")
.or_else(|_| time::strptime(s, "%A, %d-%b-%y %T %Z"))
.or_else(|_| time::strptime(s, "%c"))
{
Ok(t) => Ok(HttpDate(t)),
Err(_) => Err(ParseError::Header),
}
}
}
impl Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0.to_utc().rfc822(), f)
}
}
impl From<time::Tm> for HttpDate {
fn from(tm: time::Tm) -> HttpDate {
HttpDate(tm)
}
}
impl From<SystemTime> for HttpDate {
fn from(sys: SystemTime) -> HttpDate {
let tmspec = match sys.duration_since(UNIX_EPOCH) {
Ok(dur) => {
time::Timespec::new(dur.as_secs() as i64, dur.subsec_nanos() as i32)
}
Err(err) => {
let neg = err.duration();
time::Timespec::new(
-(neg.as_secs() as i64),
-(neg.subsec_nanos() as i32),
)
}
};
HttpDate(time::at_utc(tmspec))
}
}
impl IntoHeaderValue for HttpDate {
type Error = InvalidHeaderValueBytes;
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = BytesMut::with_capacity(29).writer();
write!(wrt, "{}", self.0.rfc822()).unwrap();
unsafe {
Ok(HeaderValue::from_shared_unchecked(
wrt.get_mut().take().freeze(),
))
}
}
}
impl From<HttpDate> for SystemTime {
fn from(date: HttpDate) -> SystemTime {
let spec = date.0.to_timespec();
if spec.sec >= 0 {
UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32)
} else {
UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32)
}
}
}
#[cfg(test)]
mod tests {
use super::HttpDate;
use time::Tm;
const NOV_07: HttpDate = HttpDate(Tm {
tm_nsec: 0,
tm_sec: 37,
tm_min: 48,
tm_hour: 8,
tm_mday: 7,
tm_mon: 10,
tm_year: 94,
tm_wday: 0,
tm_isdst: 0,
tm_yday: 0,
tm_utcoff: 0,
});
#[test]
fn test_date() {
assert_eq!(
"Sun, 07 Nov 1994 08:48:37 GMT".parse::<HttpDate>().unwrap(),
NOV_07
);
assert_eq!(
"Sunday, 07-Nov-94 08:48:37 GMT"
.parse::<HttpDate>()
.unwrap(),
NOV_07
);
assert_eq!(
"Sun Nov 7 08:48:37 1994".parse::<HttpDate>().unwrap(),
NOV_07
);
assert!("this-is-no-date".parse::<HttpDate>().is_err());
}
}

14
src/header/shared/mod.rs Normal file
View File

@ -0,0 +1,14 @@
//! Copied for `hyper::header::shared`;
pub use self::charset::Charset;
pub use self::encoding::Encoding;
pub use self::entity::EntityTag;
pub use self::httpdate::HttpDate;
pub use self::quality_item::{q, qitem, Quality, QualityItem};
pub use language_tags::LanguageTag;
mod charset;
mod encoding;
mod entity;
mod httpdate;
mod quality_item;

View File

@ -0,0 +1,296 @@
#![allow(unused, deprecated)]
use std::ascii::AsciiExt;
use std::cmp;
use std::default::Default;
use std::fmt;
use std::str;
use self::internal::IntoQuality;
/// Represents a quality used in quality values.
///
/// Can be created with the `q` function.
///
/// # Implementation notes
///
/// The quality value is defined as a number between 0 and 1 with three decimal
/// places. This means there are 1001 possible values. Since floating point
/// numbers are not exact and the smallest floating point data type (`f32`)
/// consumes four bytes, hyper uses an `u16` value to store the
/// quality internally. For performance reasons you may set quality directly to
/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality
/// `q=0.532`.
///
/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1)
/// gives more information on quality values in HTTP header fields.
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Quality(u16);
impl Default for Quality {
fn default() -> Quality {
Quality(1000)
}
}
/// Represents an item with a quality value as defined in
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1).
#[derive(Clone, PartialEq, Debug)]
pub struct QualityItem<T> {
/// The actual contents of the field.
pub item: T,
/// The quality (client or server preference) for the value.
pub quality: Quality,
}
impl<T> QualityItem<T> {
/// Creates a new `QualityItem` from an item and a quality.
/// The item can be of any type.
/// The quality should be a value in the range [0, 1].
pub fn new(item: T, quality: Quality) -> QualityItem<T> {
QualityItem { item, quality }
}
}
impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> {
fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> {
self.quality.partial_cmp(&other.quality)
}
}
impl<T: fmt::Display> fmt::Display for QualityItem<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.item, f)?;
match self.quality.0 {
1000 => Ok(()),
0 => f.write_str("; q=0"),
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_right_matches('0')),
}
}
}
impl<T: str::FromStr> str::FromStr for QualityItem<T> {
type Err = ::error::ParseError;
fn from_str(s: &str) -> Result<QualityItem<T>, ::error::ParseError> {
if !s.is_ascii() {
return Err(::error::ParseError::Header);
}
// Set defaults used if parsing fails.
let mut raw_item = s;
let mut quality = 1f32;
let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect();
if parts.len() == 2 {
if parts[0].len() < 2 {
return Err(::error::ParseError::Header);
}
let start = &parts[0][0..2];
if start == "q=" || start == "Q=" {
let q_part = &parts[0][2..parts[0].len()];
if q_part.len() > 5 {
return Err(::error::ParseError::Header);
}
match q_part.parse::<f32>() {
Ok(q_value) => {
if 0f32 <= q_value && q_value <= 1f32 {
quality = q_value;
raw_item = parts[1];
} else {
return Err(::error::ParseError::Header);
}
}
Err(_) => return Err(::error::ParseError::Header),
}
}
}
match raw_item.parse::<T>() {
// we already checked above that the quality is within range
Ok(item) => Ok(QualityItem::new(item, from_f32(quality))),
Err(_) => Err(::error::ParseError::Header),
}
}
}
#[inline]
fn from_f32(f: f32) -> Quality {
// this function is only used internally. A check that `f` is within range
// should be done before calling this method. Just in case, this
// debug_assert should catch if we were forgetful
debug_assert!(
f >= 0f32 && f <= 1f32,
"q value must be between 0.0 and 1.0"
);
Quality((f * 1000f32) as u16)
}
/// Convenience function to wrap a value in a `QualityItem`
/// Sets `q` to the default 1.0
pub fn qitem<T>(item: T) -> QualityItem<T> {
QualityItem::new(item, Default::default())
}
/// Convenience function to create a `Quality` from a float or integer.
///
/// Implemented for `u16` and `f32`. Panics if value is out of range.
pub fn q<T: IntoQuality>(val: T) -> Quality {
val.into_quality()
}
mod internal {
use super::Quality;
// TryFrom is probably better, but it's not stable. For now, we want to
// keep the functionality of the `q` function, while allowing it to be
// generic over `f32` and `u16`.
//
// `q` would panic before, so keep that behavior. `TryFrom` can be
// introduced later for a non-panicking conversion.
pub trait IntoQuality: Sealed + Sized {
fn into_quality(self) -> Quality;
}
impl IntoQuality for f32 {
fn into_quality(self) -> Quality {
assert!(
self >= 0f32 && self <= 1f32,
"float must be between 0.0 and 1.0"
);
super::from_f32(self)
}
}
impl IntoQuality for u16 {
fn into_quality(self) -> Quality {
assert!(self <= 1000, "u16 must be between 0 and 1000");
Quality(self)
}
}
pub trait Sealed {}
impl Sealed for u16 {}
impl Sealed for f32 {}
}
#[cfg(test)]
mod tests {
use super::super::encoding::*;
use super::*;
#[test]
fn test_quality_item_fmt_q_1() {
let x = qitem(Chunked);
assert_eq!(format!("{}", x), "chunked");
}
#[test]
fn test_quality_item_fmt_q_0001() {
let x = QualityItem::new(Chunked, Quality(1));
assert_eq!(format!("{}", x), "chunked; q=0.001");
}
#[test]
fn test_quality_item_fmt_q_05() {
// Custom value
let x = QualityItem {
item: EncodingExt("identity".to_owned()),
quality: Quality(500),
};
assert_eq!(format!("{}", x), "identity; q=0.5");
}
#[test]
fn test_quality_item_fmt_q_0() {
// Custom value
let x = QualityItem {
item: EncodingExt("identity".to_owned()),
quality: Quality(0),
};
assert_eq!(x.to_string(), "identity; q=0");
}
#[test]
fn test_quality_item_from_str1() {
let x: Result<QualityItem<Encoding>, _> = "chunked".parse();
assert_eq!(
x.unwrap(),
QualityItem {
item: Chunked,
quality: Quality(1000),
}
);
}
#[test]
fn test_quality_item_from_str2() {
let x: Result<QualityItem<Encoding>, _> = "chunked; q=1".parse();
assert_eq!(
x.unwrap(),
QualityItem {
item: Chunked,
quality: Quality(1000),
}
);
}
#[test]
fn test_quality_item_from_str3() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.5".parse();
assert_eq!(
x.unwrap(),
QualityItem {
item: Gzip,
quality: Quality(500),
}
);
}
#[test]
fn test_quality_item_from_str4() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.273".parse();
assert_eq!(
x.unwrap(),
QualityItem {
item: Gzip,
quality: Quality(273),
}
);
}
#[test]
fn test_quality_item_from_str5() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.2739999".parse();
assert!(x.is_err());
}
#[test]
fn test_quality_item_from_str6() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=2".parse();
assert!(x.is_err());
}
#[test]
fn test_quality_item_ordering() {
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();
let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap();
let comparision_result: bool = x.gt(&y);
assert!(comparision_result)
}
#[test]
fn test_quality() {
assert_eq!(q(0.5), Quality(500));
}
#[test]
#[should_panic] // FIXME - 32-bit msvc unwinding broken
#[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
fn test_quality_invalid() {
q(-1.0);
}
#[test]
#[should_panic] // FIXME - 32-bit msvc unwinding broken
#[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
fn test_quality_invalid2() {
q(2.0);
}
#[test]
fn test_fuzzing_bugs() {
assert!("99999;".parse::<QualityItem<String>>().is_err());
assert!("\x0d;;;=\u{d6aa}==".parse::<QualityItem<String>>().is_err())
}
}

524
src/helpers.rs Normal file
View File

@ -0,0 +1,524 @@
//! Various helpers
use http::{header, StatusCode};
use regex::Regex;
use handler::Handler;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
/// Path normalization helper
///
/// By normalizing it means:
///
/// - Add a trailing slash to the path.
/// - Remove a trailing slash from the path.
/// - Double slashes are replaced by one.
///
/// The handler returns as soon as it finds a path that resolves
/// correctly. The order if all enable is 1) merge, 3) both merge and append
/// and 3) append. If the path resolves with
/// at least one of those conditions, it will redirect to the new path.
///
/// If *append* is *true* append slash when needed. If a resource is
/// defined with trailing slash and the request comes without it, it will
/// append it automatically.
///
/// If *merge* is *true*, merge multiple consecutive slashes in the path into
/// one.
///
/// This handler designed to be use as a handler for application's *default
/// resource*.
///
/// ```rust
/// # extern crate actix_web;
/// # #[macro_use] extern crate serde_derive;
/// # use actix_web::*;
/// use actix_web::http::NormalizePath;
///
/// # fn index(req: HttpRequest) -> HttpResponse {
/// # HttpResponse::Ok().into()
/// # }
/// fn main() {
/// let app = App::new()
/// .resource("/test/", |r| r.f(index))
/// .default_resource(|r| r.h(NormalizePath::default()))
/// .finish();
/// }
/// ```
/// In this example `/test`, `/test///` will be redirected to `/test/` url.
pub struct NormalizePath {
append: bool,
merge: bool,
re_merge: Regex,
redirect: StatusCode,
not_found: StatusCode,
}
impl Default for NormalizePath {
/// Create default `NormalizePath` instance, *append* is set to *true*,
/// *merge* is set to *true* and *redirect* is set to
/// `StatusCode::MOVED_PERMANENTLY`
fn default() -> NormalizePath {
NormalizePath {
append: true,
merge: true,
re_merge: Regex::new("//+").unwrap(),
redirect: StatusCode::MOVED_PERMANENTLY,
not_found: StatusCode::NOT_FOUND,
}
}
}
impl NormalizePath {
/// Create new `NormalizePath` instance
pub fn new(append: bool, merge: bool, redirect: StatusCode) -> NormalizePath {
NormalizePath {
append,
merge,
redirect,
re_merge: Regex::new("//+").unwrap(),
not_found: StatusCode::NOT_FOUND,
}
}
}
impl<S> Handler<S> for NormalizePath {
type Result = HttpResponse;
fn handle(&mut self, req: HttpRequest<S>) -> Self::Result {
if let Some(router) = req.router() {
let query = req.query_string();
if self.merge {
// merge slashes
let p = self.re_merge.replace_all(req.path(), "/");
if p.len() != req.path().len() {
if router.has_route(p.as_ref()) {
let p = if !query.is_empty() {
p + "?" + query
} else {
p
};
return HttpResponse::build(self.redirect)
.header(header::LOCATION, p.as_ref())
.finish();
}
// merge slashes and append trailing slash
if self.append && !p.ends_with('/') {
let p = p.as_ref().to_owned() + "/";
if router.has_route(&p) {
let p = if !query.is_empty() {
p + "?" + query
} else {
p
};
return HttpResponse::build(self.redirect)
.header(header::LOCATION, p.as_str())
.finish();
}
}
// try to remove trailing slash
if p.ends_with('/') {
let p = p.as_ref().trim_right_matches('/');
if router.has_route(p) {
let mut req = HttpResponse::build(self.redirect);
return if !query.is_empty() {
req.header(
header::LOCATION,
(p.to_owned() + "?" + query).as_str(),
)
} else {
req.header(header::LOCATION, p)
}.finish();
}
}
} else if p.ends_with('/') {
// try to remove trailing slash
let p = p.as_ref().trim_right_matches('/');
if router.has_route(p) {
let mut req = HttpResponse::build(self.redirect);
return if !query.is_empty() {
req.header(
header::LOCATION,
(p.to_owned() + "?" + query).as_str(),
)
} else {
req.header(header::LOCATION, p)
}.finish();
}
}
}
// append trailing slash
if self.append && !req.path().ends_with('/') {
let p = req.path().to_owned() + "/";
if router.has_route(&p) {
let p = if !query.is_empty() {
p + "?" + query
} else {
p
};
return HttpResponse::build(self.redirect)
.header(header::LOCATION, p.as_str())
.finish();
}
}
}
HttpResponse::new(self.not_found)
}
}
#[cfg(test)]
mod tests {
use super::*;
use application::App;
use http::{header, Method};
use test::TestRequest;
fn index(_req: HttpRequest) -> HttpResponse {
HttpResponse::new(StatusCode::OK)
}
#[test]
fn test_normalize_path_trailing_slashes() {
let mut app = App::new()
.resource("/resource1", |r| r.method(Method::GET).f(index))
.resource("/resource2/", |r| r.method(Method::GET).f(index))
.default_resource(|r| r.h(NormalizePath::default()))
.finish();
// trailing slashes
let params = vec![
("/resource1", "", StatusCode::OK),
("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY),
("/resource2", "/resource2/", StatusCode::MOVED_PERMANENTLY),
("/resource2/", "", StatusCode::OK),
("/resource1?p1=1&p2=2", "", StatusCode::OK),
(
"/resource1/?p1=1&p2=2",
"/resource1?p1=1&p2=2",
StatusCode::MOVED_PERMANENTLY,
),
(
"/resource2?p1=1&p2=2",
"/resource2/?p1=1&p2=2",
StatusCode::MOVED_PERMANENTLY,
),
("/resource2/?p1=1&p2=2", "", StatusCode::OK),
];
for (path, target, code) in params {
let req = app.prepare_request(TestRequest::with_uri(path).finish());
let resp = app.run(req);
let r = resp.as_msg();
assert_eq!(r.status(), code);
if !target.is_empty() {
assert_eq!(
target,
r.headers().get(header::LOCATION).unwrap().to_str().unwrap()
);
}
}
}
#[test]
fn test_normalize_path_trailing_slashes_disabled() {
let mut app = App::new()
.resource("/resource1", |r| r.method(Method::GET).f(index))
.resource("/resource2/", |r| r.method(Method::GET).f(index))
.default_resource(|r| {
r.h(NormalizePath::new(
false,
true,
StatusCode::MOVED_PERMANENTLY,
))
})
.finish();
// trailing slashes
let params = vec![
("/resource1", StatusCode::OK),
("/resource1/", StatusCode::MOVED_PERMANENTLY),
("/resource2", StatusCode::NOT_FOUND),
("/resource2/", StatusCode::OK),
("/resource1?p1=1&p2=2", StatusCode::OK),
("/resource1/?p1=1&p2=2", StatusCode::MOVED_PERMANENTLY),
("/resource2?p1=1&p2=2", StatusCode::NOT_FOUND),
("/resource2/?p1=1&p2=2", StatusCode::OK),
];
for (path, code) in params {
let req = app.prepare_request(TestRequest::with_uri(path).finish());
let resp = app.run(req);
let r = resp.as_msg();
assert_eq!(r.status(), code);
}
}
#[test]
fn test_normalize_path_merge_slashes() {
let mut app = App::new()
.resource("/resource1", |r| r.method(Method::GET).f(index))
.resource("/resource1/a/b", |r| r.method(Method::GET).f(index))
.default_resource(|r| r.h(NormalizePath::default()))
.finish();
// trailing slashes
let params = vec![
("/resource1/a/b", "", StatusCode::OK),
("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY),
("/resource1//", "/resource1", StatusCode::MOVED_PERMANENTLY),
(
"//resource1//a//b",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource1//a//b/",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource1//a//b//",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource1//a//b",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a///b",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a//b/",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
("/resource1/a/b?p=1", "", StatusCode::OK),
(
"//resource1//a//b?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource1//a//b/?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource1//a//b?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a///b?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a//b/?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a//b//?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
];
for (path, target, code) in params {
let req = app.prepare_request(TestRequest::with_uri(path).finish());
let resp = app.run(req);
let r = resp.as_msg();
assert_eq!(r.status(), code);
if !target.is_empty() {
assert_eq!(
target,
r.headers().get(header::LOCATION).unwrap().to_str().unwrap()
);
}
}
}
#[test]
fn test_normalize_path_merge_and_append_slashes() {
let mut app = App::new()
.resource("/resource1", |r| r.method(Method::GET).f(index))
.resource("/resource2/", |r| r.method(Method::GET).f(index))
.resource("/resource1/a/b", |r| r.method(Method::GET).f(index))
.resource("/resource2/a/b/", |r| r.method(Method::GET).f(index))
.default_resource(|r| r.h(NormalizePath::default()))
.finish();
// trailing slashes
let params = vec![
("/resource1/a/b", "", StatusCode::OK),
(
"/resource1/a/b/",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource2//a//b",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource2//a//b/",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource2//a//b//",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource1//a//b",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource1//a//b/",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a///b",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a///b/",
"/resource1/a/b",
StatusCode::MOVED_PERMANENTLY,
),
(
"/resource2/a/b",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
("/resource2/a/b/", "", StatusCode::OK),
(
"//resource2//a//b",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource2//a//b/",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource2//a//b",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource2//a//b/",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource2/a///b",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource2/a///b/",
"/resource2/a/b/",
StatusCode::MOVED_PERMANENTLY,
),
("/resource1/a/b?p=1", "", StatusCode::OK),
(
"/resource1/a/b/?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource2//a//b?p=1",
"/resource2/a/b/?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource2//a//b/?p=1",
"/resource2/a/b/?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource1//a//b?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource1//a//b/?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a///b?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a///b/?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource1/a///b//?p=1",
"/resource1/a/b?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"/resource2/a/b?p=1",
"/resource2/a/b/?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource2//a//b?p=1",
"/resource2/a/b/?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"//resource2//a//b/?p=1",
"/resource2/a/b/?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource2//a//b?p=1",
"/resource2/a/b/?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"///resource2//a//b/?p=1",
"/resource2/a/b/?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource2/a///b?p=1",
"/resource2/a/b/?p=1",
StatusCode::MOVED_PERMANENTLY,
),
(
"/////resource2/a///b/?p=1",
"/resource2/a/b/?p=1",
StatusCode::MOVED_PERMANENTLY,
),
];
for (path, target, code) in params {
let req = app.prepare_request(TestRequest::with_uri(path).finish());
let resp = app.run(req);
let r = resp.as_msg();
assert_eq!(r.status(), code);
if !target.is_empty() {
assert_eq!(
target,
r.headers().get(header::LOCATION).unwrap().to_str().unwrap()
);
}
}
}
}

View File

@ -1,118 +1,84 @@
//! Basic http responses
#![allow(non_upper_case_globals)]
use std::rc::Rc;
use http::StatusCode;
use body::Body;
use task::Task;
use route::RouteHandler;
use payload::Payload;
use httprequest::HttpRequest;
use httpresponse::{HttpResponse, HttpResponseBuilder};
pub const HTTPOk: StaticResponse = StaticResponse(StatusCode::OK);
pub const HTTPCreated: StaticResponse = StaticResponse(StatusCode::CREATED);
pub const HTTPNoContent: StaticResponse = StaticResponse(StatusCode::NO_CONTENT);
pub const HTTPMultipleChoices: StaticResponse = StaticResponse(StatusCode::MULTIPLE_CHOICES);
pub const HTTPMovedPermanenty: StaticResponse = StaticResponse(StatusCode::MOVED_PERMANENTLY);
pub const HTTPFound: StaticResponse = StaticResponse(StatusCode::FOUND);
pub const HTTPSeeOther: StaticResponse = StaticResponse(StatusCode::SEE_OTHER);
pub const HTTPNotModified: StaticResponse = StaticResponse(StatusCode::NOT_MODIFIED);
pub const HTTPUseProxy: StaticResponse = StaticResponse(StatusCode::USE_PROXY);
pub const HTTPTemporaryRedirect: StaticResponse =
StaticResponse(StatusCode::TEMPORARY_REDIRECT);
pub const HTTPPermanentRedirect: StaticResponse =
StaticResponse(StatusCode::PERMANENT_REDIRECT);
pub const HTTPBadRequest: StaticResponse = StaticResponse(StatusCode::BAD_REQUEST);
pub const HTTPNotFound: StaticResponse = StaticResponse(StatusCode::NOT_FOUND);
pub const HTTPUnauthorized: StaticResponse = StaticResponse(StatusCode::UNAUTHORIZED);
pub const HTTPPaymentRequired: StaticResponse = StaticResponse(StatusCode::PAYMENT_REQUIRED);
pub const HTTPForbidden: StaticResponse = StaticResponse(StatusCode::FORBIDDEN);
pub const HTTPMethodNotAllowed: StaticResponse =
StaticResponse(StatusCode::METHOD_NOT_ALLOWED);
pub const HTTPNotAcceptable: StaticResponse = StaticResponse(StatusCode::NOT_ACCEPTABLE);
pub const HTTPProxyAuthenticationRequired: StaticResponse =
StaticResponse(StatusCode::PROXY_AUTHENTICATION_REQUIRED);
pub const HTTPRequestTimeout: StaticResponse = StaticResponse(StatusCode::REQUEST_TIMEOUT);
pub const HTTPConflict: StaticResponse = StaticResponse(StatusCode::CONFLICT);
pub const HTTPGone: StaticResponse = StaticResponse(StatusCode::GONE);
pub const HTTPLengthRequired: StaticResponse = StaticResponse(StatusCode::LENGTH_REQUIRED);
pub const HTTPPreconditionFailed: StaticResponse =
StaticResponse(StatusCode::PRECONDITION_FAILED);
pub const HTTPPayloadTooLarge: StaticResponse = StaticResponse(StatusCode::PAYLOAD_TOO_LARGE);
pub const HTTPUriTooLong: StaticResponse = StaticResponse(StatusCode::URI_TOO_LONG);
pub const HTTPExpectationFailed: StaticResponse =
StaticResponse(StatusCode::EXPECTATION_FAILED);
pub const HTTPInternalServerError: StaticResponse =
StaticResponse(StatusCode::INTERNAL_SERVER_ERROR);
pub struct StaticResponse(StatusCode);
impl StaticResponse {
pub fn builder(&self) -> HttpResponseBuilder {
HttpResponse::builder(self.0)
}
pub fn response(&self) -> HttpResponse {
HttpResponse::new(self.0, Body::Empty)
}
pub fn with_reason(self, reason: &'static str) -> HttpResponse {
let mut resp = HttpResponse::new(self.0, Body::Empty);
resp.set_reason(reason);
resp
}
pub fn with_body<B: Into<Body>>(self, body: B) -> HttpResponse {
HttpResponse::new(self.0, body.into())
}
macro_rules! STATIC_RESP {
($name:ident, $status:expr) => {
#[allow(non_snake_case)]
pub fn $name() -> HttpResponseBuilder {
HttpResponse::build($status)
}
};
}
impl<S> RouteHandler<S> for StaticResponse {
fn handle(&self, _: &mut HttpRequest, _: Payload, _: Rc<S>) -> Task {
Task::reply(HttpResponse::new(self.0, Body::Empty))
}
}
impl HttpResponse {
STATIC_RESP!(Ok, StatusCode::OK);
STATIC_RESP!(Created, StatusCode::CREATED);
STATIC_RESP!(Accepted, StatusCode::ACCEPTED);
STATIC_RESP!(
NonAuthoritativeInformation,
StatusCode::NON_AUTHORITATIVE_INFORMATION
);
impl From<StaticResponse> for HttpResponse {
fn from(st: StaticResponse) -> Self {
st.response()
}
}
STATIC_RESP!(NoContent, StatusCode::NO_CONTENT);
STATIC_RESP!(ResetContent, StatusCode::RESET_CONTENT);
STATIC_RESP!(PartialContent, StatusCode::PARTIAL_CONTENT);
STATIC_RESP!(MultiStatus, StatusCode::MULTI_STATUS);
STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED);
STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES);
STATIC_RESP!(MovedPermanenty, StatusCode::MOVED_PERMANENTLY);
STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY);
STATIC_RESP!(Found, StatusCode::FOUND);
STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER);
STATIC_RESP!(NotModified, StatusCode::NOT_MODIFIED);
STATIC_RESP!(UseProxy, StatusCode::USE_PROXY);
STATIC_RESP!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT);
STATIC_RESP!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT);
STATIC_RESP!(BadRequest, StatusCode::BAD_REQUEST);
STATIC_RESP!(NotFound, StatusCode::NOT_FOUND);
STATIC_RESP!(Unauthorized, StatusCode::UNAUTHORIZED);
STATIC_RESP!(PaymentRequired, StatusCode::PAYMENT_REQUIRED);
STATIC_RESP!(Forbidden, StatusCode::FORBIDDEN);
STATIC_RESP!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED);
STATIC_RESP!(NotAcceptable, StatusCode::NOT_ACCEPTABLE);
STATIC_RESP!(
ProxyAuthenticationRequired,
StatusCode::PROXY_AUTHENTICATION_REQUIRED
);
STATIC_RESP!(RequestTimeout, StatusCode::REQUEST_TIMEOUT);
STATIC_RESP!(Conflict, StatusCode::CONFLICT);
STATIC_RESP!(Gone, StatusCode::GONE);
STATIC_RESP!(LengthRequired, StatusCode::LENGTH_REQUIRED);
STATIC_RESP!(PreconditionFailed, StatusCode::PRECONDITION_FAILED);
STATIC_RESP!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE);
STATIC_RESP!(UriTooLong, StatusCode::URI_TOO_LONG);
STATIC_RESP!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE);
STATIC_RESP!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE);
STATIC_RESP!(ExpectationFailed, StatusCode::EXPECTATION_FAILED);
STATIC_RESP!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR);
STATIC_RESP!(NotImplemented, StatusCode::NOT_IMPLEMENTED);
STATIC_RESP!(BadGateway, StatusCode::BAD_GATEWAY);
STATIC_RESP!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE);
STATIC_RESP!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT);
STATIC_RESP!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED);
STATIC_RESP!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES);
STATIC_RESP!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE);
STATIC_RESP!(LoopDetected, StatusCode::LOOP_DETECTED);
}
#[cfg(test)]
mod tests {
use body::Body;
use http::StatusCode;
use super::{HTTPOk, HTTPBadRequest, Body, HttpResponse};
use httpresponse::HttpResponse;
#[test]
fn test_builder() {
let resp = HTTPOk.builder().body(Body::Empty).unwrap();
fn test_build() {
let resp = HttpResponse::Ok().body(Body::Empty);
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_response() {
let resp = HTTPOk.response();
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_from() {
let resp: HttpResponse = HTTPOk.into();
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_with_reason() {
let resp = HTTPOk.response();
assert_eq!(resp.reason(), "");
let resp = HTTPBadRequest.with_reason("test");
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert_eq!(resp.reason(), "test");
}
}

668
src/httpmessage.rs Normal file
View File

@ -0,0 +1,668 @@
use bytes::{Bytes, BytesMut};
use encoding::all::UTF_8;
use encoding::label::encoding_from_whatwg_label;
use encoding::types::{DecoderTrap, Encoding};
use encoding::EncodingRef;
use futures::{Future, Poll, Stream};
use http::{header, HeaderMap};
use http_range::HttpRange;
use mime::Mime;
use serde::de::DeserializeOwned;
use serde_urlencoded;
use std::str;
use error::{
ContentTypeError, HttpRangeError, ParseError, PayloadError, UrlencodedError,
};
use header::Header;
use json::JsonBody;
use multipart::Multipart;
/// Trait that implements general purpose operations on http messages
pub trait HttpMessage {
/// Read the message headers.
fn headers(&self) -> &HeaderMap;
#[doc(hidden)]
/// Get a header
fn get_header<H: Header>(&self) -> Option<H>
where
Self: Sized,
{
if self.headers().contains_key(H::name()) {
H::parse(self).ok()
} else {
None
}
}
/// Read the request content type. If request does not contain
/// *Content-Type* header, empty str get returned.
fn content_type(&self) -> &str {
if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) {
if let Ok(content_type) = content_type.to_str() {
return content_type.split(';').next().unwrap().trim();
}
}
""
}
/// Get content type encoding
///
/// UTF-8 is used by default, If request charset is not set.
fn encoding(&self) -> Result<EncodingRef, ContentTypeError> {
if let Some(mime_type) = self.mime_type()? {
if let Some(charset) = mime_type.get_param("charset") {
if let Some(enc) = encoding_from_whatwg_label(charset.as_str()) {
Ok(enc)
} else {
Err(ContentTypeError::UnknownEncoding)
}
} else {
Ok(UTF_8)
}
} else {
Ok(UTF_8)
}
}
/// Convert the request content type to a known mime type.
fn mime_type(&self) -> Result<Option<Mime>, ContentTypeError> {
if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) {
if let Ok(content_type) = content_type.to_str() {
return match content_type.parse() {
Ok(mt) => Ok(Some(mt)),
Err(_) => Err(ContentTypeError::ParseError),
};
} else {
return Err(ContentTypeError::ParseError);
}
}
Ok(None)
}
/// Check if request has chunked transfer encoding
fn chunked(&self) -> Result<bool, ParseError> {
if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) {
if let Ok(s) = encodings.to_str() {
Ok(s.to_lowercase().contains("chunked"))
} else {
Err(ParseError::Header)
}
} else {
Ok(false)
}
}
/// Parses Range HTTP header string as per RFC 2616.
/// `size` is full size of response (file).
fn range(&self, size: u64) -> Result<Vec<HttpRange>, HttpRangeError> {
if let Some(range) = self.headers().get(header::RANGE) {
HttpRange::parse(unsafe { str::from_utf8_unchecked(range.as_bytes()) }, size)
.map_err(|e| e.into())
} else {
Ok(Vec::new())
}
}
/// Load http message body.
///
/// By default only 256Kb payload reads to a memory, then
/// `PayloadError::Overflow` get returned. Use `MessageBody::limit()`
/// method to change upper limit.
///
/// ## Server example
///
/// ```rust
/// # extern crate bytes;
/// # extern crate actix_web;
/// # extern crate futures;
/// # #[macro_use] extern crate serde_derive;
/// use bytes::Bytes;
/// use futures::future::Future;
/// use actix_web::{HttpMessage, HttpRequest, HttpResponse,
/// FutureResponse, AsyncResponder};
///
/// fn index(mut req: HttpRequest) -> FutureResponse<HttpResponse> {
/// req.body() // <- get Body future
/// .limit(1024) // <- change max size of the body to a 1kb
/// .from_err()
/// .and_then(|bytes: Bytes| { // <- complete body
/// println!("==== BODY ==== {:?}", bytes);
/// Ok(HttpResponse::Ok().into())
/// }).responder()
/// }
/// # fn main() {}
/// ```
fn body(self) -> MessageBody<Self>
where
Self: Stream<Item = Bytes, Error = PayloadError> + Sized,
{
MessageBody::new(self)
}
/// Parse `application/x-www-form-urlencoded` encoded request's body.
/// Return `UrlEncoded` future. Form can be deserialized to any type that
/// implements `Deserialize` trait from *serde*.
///
/// Returns error:
///
/// * content type is not `application/x-www-form-urlencoded`
/// * content-length is greater than 256k
///
/// ## Server example
///
/// ```rust
/// # extern crate actix_web;
/// # extern crate futures;
/// # use futures::Future;
/// # use std::collections::HashMap;
/// use actix_web::{HttpMessage, HttpRequest, HttpResponse, FutureResponse};
///
/// fn index(mut req: HttpRequest) -> FutureResponse<HttpResponse> {
/// Box::new(
/// req.urlencoded::<HashMap<String, String>>() // <- get UrlEncoded future
/// .from_err()
/// .and_then(|params| { // <- url encoded parameters
/// println!("==== BODY ==== {:?}", params);
/// Ok(HttpResponse::Ok().into())
/// }))
/// }
/// # fn main() {}
/// ```
fn urlencoded<T: DeserializeOwned>(self) -> UrlEncoded<Self, T>
where
Self: Stream<Item = Bytes, Error = PayloadError> + Sized,
{
UrlEncoded::new(self)
}
/// Parse `application/json` encoded body.
/// Return `JsonBody<T>` future. It resolves to a `T` value.
///
/// Returns error:
///
/// * content type is not `application/json`
/// * content length is greater than 256k
///
/// ## Server example
///
/// ```rust
/// # extern crate actix_web;
/// # extern crate futures;
/// # #[macro_use] extern crate serde_derive;
/// use actix_web::*;
/// use futures::future::{Future, ok};
///
/// #[derive(Deserialize, Debug)]
/// struct MyObj {
/// name: String,
/// }
///
/// fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
/// req.json() // <- get JsonBody future
/// .from_err()
/// .and_then(|val: MyObj| { // <- deserialized value
/// println!("==== BODY ==== {:?}", val);
/// Ok(HttpResponse::Ok().into())
/// }).responder()
/// }
/// # fn main() {}
/// ```
fn json<T: DeserializeOwned>(self) -> JsonBody<Self, T>
where
Self: Stream<Item = Bytes, Error = PayloadError> + Sized,
{
JsonBody::new(self)
}
/// Return stream to http payload processes as multipart.
///
/// Content-type: multipart/form-data;
///
/// ## Server example
///
/// ```rust
/// # extern crate actix;
/// # extern crate actix_web;
/// # extern crate env_logger;
/// # extern crate futures;
/// # use std::str;
/// # use actix::*;
/// # use actix_web::*;
/// # use futures::{Future, Stream};
/// # use futures::future::{ok, result, Either};
/// fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
/// req.multipart().from_err() // <- get multipart stream for current request
/// .and_then(|item| match item { // <- iterate over multipart items
/// multipart::MultipartItem::Field(field) => {
/// // Field in turn is stream of *Bytes* object
/// Either::A(field.from_err()
/// .map(|c| println!("-- CHUNK: \n{:?}", str::from_utf8(&c)))
/// .finish())
/// },
/// multipart::MultipartItem::Nested(mp) => {
/// // Or item could be nested Multipart stream
/// Either::B(ok(()))
/// }
/// })
/// .finish() // <- Stream::finish() combinator from actix
/// .map(|_| HttpResponse::Ok().into())
/// .responder()
/// }
/// # fn main() {}
/// ```
fn multipart(self) -> Multipart<Self>
where
Self: Stream<Item = Bytes, Error = PayloadError> + Sized,
{
let boundary = Multipart::boundary(self.headers());
Multipart::new(boundary, self)
}
}
/// Future that resolves to a complete http message body.
pub struct MessageBody<T> {
limit: usize,
req: Option<T>,
fut: Option<Box<Future<Item = Bytes, Error = PayloadError>>>,
}
impl<T> MessageBody<T> {
/// Create `RequestBody` for request.
pub fn new(req: T) -> MessageBody<T> {
MessageBody {
limit: 262_144,
req: Some(req),
fut: None,
}
}
/// Change max size of payload. By default max size is 256Kb
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
}
impl<T> Future for MessageBody<T>
where
T: HttpMessage + Stream<Item = Bytes, Error = PayloadError> + 'static,
{
type Item = Bytes;
type Error = PayloadError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
if let Some(req) = self.req.take() {
if let Some(len) = req.headers().get(header::CONTENT_LENGTH) {
if let Ok(s) = len.to_str() {
if let Ok(len) = s.parse::<usize>() {
if len > self.limit {
return Err(PayloadError::Overflow);
}
} else {
return Err(PayloadError::UnknownLength);
}
} else {
return Err(PayloadError::UnknownLength);
}
}
// future
let limit = self.limit;
self.fut = Some(Box::new(
req.from_err()
.fold(BytesMut::new(), move |mut body, chunk| {
if (body.len() + chunk.len()) > limit {
Err(PayloadError::Overflow)
} else {
body.extend_from_slice(&chunk);
Ok(body)
}
})
.map(|body| body.freeze()),
));
}
self.fut
.as_mut()
.expect("UrlEncoded could not be used second time")
.poll()
}
}
/// Future that resolves to a parsed urlencoded values.
pub struct UrlEncoded<T, U> {
req: Option<T>,
limit: usize,
fut: Option<Box<Future<Item = U, Error = UrlencodedError>>>,
}
impl<T, U> UrlEncoded<T, U> {
pub fn new(req: T) -> UrlEncoded<T, U> {
UrlEncoded {
req: Some(req),
limit: 262_144,
fut: None,
}
}
/// Change max size of payload. By default max size is 256Kb
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
}
impl<T, U> Future for UrlEncoded<T, U>
where
T: HttpMessage + Stream<Item = Bytes, Error = PayloadError> + 'static,
U: DeserializeOwned + 'static,
{
type Item = U;
type Error = UrlencodedError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
if let Some(req) = self.req.take() {
if let Some(len) = req.headers().get(header::CONTENT_LENGTH) {
if let Ok(s) = len.to_str() {
if let Ok(len) = s.parse::<u64>() {
if len > 262_144 {
return Err(UrlencodedError::Overflow);
}
} else {
return Err(UrlencodedError::UnknownLength);
}
} else {
return Err(UrlencodedError::UnknownLength);
}
}
// check content type
if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" {
return Err(UrlencodedError::ContentType);
}
let encoding = req.encoding().map_err(|_| UrlencodedError::ContentType)?;
// future
let limit = self.limit;
let fut = req
.from_err()
.fold(BytesMut::new(), move |mut body, chunk| {
if (body.len() + chunk.len()) > limit {
Err(UrlencodedError::Overflow)
} else {
body.extend_from_slice(&chunk);
Ok(body)
}
})
.and_then(move |body| {
let enc: *const Encoding = encoding as *const Encoding;
if enc == UTF_8 {
serde_urlencoded::from_bytes::<U>(&body)
.map_err(|_| UrlencodedError::Parse)
} else {
let body = encoding
.decode(&body, DecoderTrap::Strict)
.map_err(|_| UrlencodedError::Parse)?;
serde_urlencoded::from_str::<U>(&body)
.map_err(|_| UrlencodedError::Parse)
}
});
self.fut = Some(Box::new(fut));
}
self.fut
.as_mut()
.expect("UrlEncoded could not be used second time")
.poll()
}
}
#[cfg(test)]
mod tests {
use super::*;
use encoding::all::ISO_8859_2;
use encoding::Encoding;
use futures::Async;
use http::{Method, Uri, Version};
use httprequest::HttpRequest;
use mime;
use std::str::FromStr;
use test::TestRequest;
#[test]
fn test_content_type() {
let req = TestRequest::with_header("content-type", "text/plain").finish();
assert_eq!(req.content_type(), "text/plain");
let req =
TestRequest::with_header("content-type", "application/json; charset=utf=8")
.finish();
assert_eq!(req.content_type(), "application/json");
let req = HttpRequest::default();
assert_eq!(req.content_type(), "");
}
#[test]
fn test_mime_type() {
let req = TestRequest::with_header("content-type", "application/json").finish();
assert_eq!(req.mime_type().unwrap(), Some(mime::APPLICATION_JSON));
let req = HttpRequest::default();
assert_eq!(req.mime_type().unwrap(), None);
let req =
TestRequest::with_header("content-type", "application/json; charset=utf-8")
.finish();
let mt = req.mime_type().unwrap().unwrap();
assert_eq!(mt.get_param(mime::CHARSET), Some(mime::UTF_8));
assert_eq!(mt.type_(), mime::APPLICATION);
assert_eq!(mt.subtype(), mime::JSON);
}
#[test]
fn test_mime_type_error() {
let req = TestRequest::with_header(
"content-type",
"applicationadfadsfasdflknadsfklnadsfjson",
).finish();
assert_eq!(Err(ContentTypeError::ParseError), req.mime_type());
}
#[test]
fn test_encoding() {
let req = HttpRequest::default();
assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
let req = TestRequest::with_header("content-type", "application/json").finish();
assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
let req = TestRequest::with_header(
"content-type",
"application/json; charset=ISO-8859-2",
).finish();
assert_eq!(ISO_8859_2.name(), req.encoding().unwrap().name());
}
#[test]
fn test_encoding_error() {
let req = TestRequest::with_header("content-type", "applicatjson").finish();
assert_eq!(Some(ContentTypeError::ParseError), req.encoding().err());
let req = TestRequest::with_header(
"content-type",
"application/json; charset=kkkttktk",
).finish();
assert_eq!(
Some(ContentTypeError::UnknownEncoding),
req.encoding().err()
);
}
#[test]
fn test_no_request_range_header() {
let req = HttpRequest::default();
let ranges = req.range(100).unwrap();
assert!(ranges.is_empty());
}
#[test]
fn test_request_range_header() {
let req = TestRequest::with_header(header::RANGE, "bytes=0-4").finish();
let ranges = req.range(100).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start, 0);
assert_eq!(ranges[0].length, 5);
}
#[test]
fn test_chunked() {
let req = HttpRequest::default();
assert!(!req.chunked().unwrap());
let req =
TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish();
assert!(req.chunked().unwrap());
let mut headers = HeaderMap::new();
let s = unsafe {
str::from_utf8_unchecked(b"some va\xadscc\xacas0xsdasdlue".as_ref())
};
headers.insert(
header::TRANSFER_ENCODING,
header::HeaderValue::from_str(s).unwrap(),
);
let req = HttpRequest::new(
Method::GET,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
headers,
None,
);
assert!(req.chunked().is_err());
}
impl PartialEq for UrlencodedError {
fn eq(&self, other: &UrlencodedError) -> bool {
match *self {
UrlencodedError::Chunked => match *other {
UrlencodedError::Chunked => true,
_ => false,
},
UrlencodedError::Overflow => match *other {
UrlencodedError::Overflow => true,
_ => false,
},
UrlencodedError::UnknownLength => match *other {
UrlencodedError::UnknownLength => true,
_ => false,
},
UrlencodedError::ContentType => match *other {
UrlencodedError::ContentType => true,
_ => false,
},
_ => false,
}
}
}
#[derive(Deserialize, Debug, PartialEq)]
struct Info {
hello: String,
}
#[test]
fn test_urlencoded_error() {
let req = TestRequest::with_header(
header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
).header(header::CONTENT_LENGTH, "xxxx")
.finish();
assert_eq!(
req.urlencoded::<Info>().poll().err().unwrap(),
UrlencodedError::UnknownLength
);
let req = TestRequest::with_header(
header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
).header(header::CONTENT_LENGTH, "1000000")
.finish();
assert_eq!(
req.urlencoded::<Info>().poll().err().unwrap(),
UrlencodedError::Overflow
);
let req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain")
.header(header::CONTENT_LENGTH, "10")
.finish();
assert_eq!(
req.urlencoded::<Info>().poll().err().unwrap(),
UrlencodedError::ContentType
);
}
#[test]
fn test_urlencoded() {
let mut req = TestRequest::with_header(
header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
).header(header::CONTENT_LENGTH, "11")
.finish();
req.payload_mut()
.unread_data(Bytes::from_static(b"hello=world"));
let result = req.urlencoded::<Info>().poll().ok().unwrap();
assert_eq!(
result,
Async::Ready(Info {
hello: "world".to_owned()
})
);
let mut req = TestRequest::with_header(
header::CONTENT_TYPE,
"application/x-www-form-urlencoded; charset=utf-8",
).header(header::CONTENT_LENGTH, "11")
.finish();
req.payload_mut()
.unread_data(Bytes::from_static(b"hello=world"));
let result = req.urlencoded().poll().ok().unwrap();
assert_eq!(
result,
Async::Ready(Info {
hello: "world".to_owned()
})
);
}
#[test]
fn test_message_body() {
let req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish();
match req.body().poll().err().unwrap() {
PayloadError::UnknownLength => (),
_ => unreachable!("error"),
}
let req = TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish();
match req.body().poll().err().unwrap() {
PayloadError::Overflow => (),
_ => unreachable!("error"),
}
let mut req = HttpRequest::default();
req.payload_mut().unread_data(Bytes::from_static(b"test"));
match req.body().poll().ok().unwrap() {
Async::Ready(bytes) => assert_eq!(bytes, Bytes::from_static(b"test")),
_ => unreachable!("error"),
}
let mut req = HttpRequest::default();
req.payload_mut()
.unread_data(Bytes::from_static(b"11111111111111"));
match req.body().limit(5).poll().err().unwrap() {
PayloadError::Overflow => (),
_ => unreachable!("error"),
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

228
src/info.rs Normal file
View File

@ -0,0 +1,228 @@
use http::header::{self, HeaderName};
use httpmessage::HttpMessage;
use httprequest::HttpRequest;
use std::str::FromStr;
const X_FORWARDED_FOR: &str = "X-FORWARDED-FOR";
const X_FORWARDED_HOST: &str = "X-FORWARDED-HOST";
const X_FORWARDED_PROTO: &str = "X-FORWARDED-PROTO";
/// `HttpRequest` connection information
pub struct ConnectionInfo<'a> {
scheme: &'a str,
host: &'a str,
remote: Option<&'a str>,
peer: Option<String>,
}
impl<'a> ConnectionInfo<'a> {
/// Create *ConnectionInfo* instance for a request.
#[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))]
pub fn new<S>(req: &'a HttpRequest<S>) -> ConnectionInfo<'a> {
let mut host = None;
let mut scheme = None;
let mut remote = None;
let mut peer = None;
// load forwarded header
for hdr in req.headers().get_all(header::FORWARDED) {
if let Ok(val) = hdr.to_str() {
for pair in val.split(';') {
for el in pair.split(',') {
let mut items = el.trim().splitn(2, '=');
if let Some(name) = items.next() {
if let Some(val) = items.next() {
match &name.to_lowercase() as &str {
"for" => if remote.is_none() {
remote = Some(val.trim());
},
"proto" => if scheme.is_none() {
scheme = Some(val.trim());
},
"host" => if host.is_none() {
host = Some(val.trim());
},
_ => (),
}
}
}
}
}
}
}
// scheme
if scheme.is_none() {
if let Some(h) = req
.headers()
.get(HeaderName::from_str(X_FORWARDED_PROTO).unwrap())
{
if let Ok(h) = h.to_str() {
scheme = h.split(',').next().map(|v| v.trim());
}
}
if scheme.is_none() {
scheme = req.uri().scheme_part().map(|a| a.as_str());
if scheme.is_none() {
if let Some(router) = req.router() {
if router.server_settings().secure() {
scheme = Some("https")
}
}
}
}
}
// host
if host.is_none() {
if let Some(h) = req
.headers()
.get(HeaderName::from_str(X_FORWARDED_HOST).unwrap())
{
if let Ok(h) = h.to_str() {
host = h.split(',').next().map(|v| v.trim());
}
}
if host.is_none() {
if let Some(h) = req.headers().get(header::HOST) {
host = h.to_str().ok();
}
if host.is_none() {
host = req.uri().authority_part().map(|a| a.as_str());
if host.is_none() {
if let Some(router) = req.router() {
host = Some(router.server_settings().host());
}
}
}
}
}
// remote addr
if remote.is_none() {
if let Some(h) = req
.headers()
.get(HeaderName::from_str(X_FORWARDED_FOR).unwrap())
{
if let Ok(h) = h.to_str() {
remote = h.split(',').next().map(|v| v.trim());
}
}
if remote.is_none() {
// get peeraddr from socketaddr
peer = req.peer_addr().map(|addr| format!("{}", addr));
}
}
ConnectionInfo {
scheme: scheme.unwrap_or("http"),
host: host.unwrap_or("localhost"),
remote,
peer,
}
}
/// Scheme of the request.
///
/// Scheme is resolved through the following headers, in this order:
///
/// - Forwarded
/// - X-Forwarded-Proto
/// - Uri
#[inline]
pub fn scheme(&self) -> &str {
self.scheme
}
/// Hostname of the request.
///
/// Hostname is resolved through the following headers, in this order:
///
/// - Forwarded
/// - X-Forwarded-Host
/// - Host
/// - Uri
/// - Server hostname
pub fn host(&self) -> &str {
self.host
}
/// Remote IP of client initiated HTTP request.
///
/// The IP is resolved through the following headers, in this order:
///
/// - Forwarded
/// - X-Forwarded-For
/// - peer name of opened socket
#[inline]
pub fn remote(&self) -> Option<&str> {
if let Some(r) = self.remote {
Some(r)
} else if let Some(ref peer) = self.peer {
Some(peer)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::header::HeaderValue;
#[test]
fn test_forwarded() {
let req = HttpRequest::default();
let info = ConnectionInfo::new(&req);
assert_eq!(info.scheme(), "http");
assert_eq!(info.host(), "localhost");
let mut req = HttpRequest::default();
req.headers_mut().insert(
header::FORWARDED,
HeaderValue::from_static(
"for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org",
),
);
let info = ConnectionInfo::new(&req);
assert_eq!(info.scheme(), "https");
assert_eq!(info.host(), "rust-lang.org");
assert_eq!(info.remote(), Some("192.0.2.60"));
let mut req = HttpRequest::default();
req.headers_mut()
.insert(header::HOST, HeaderValue::from_static("rust-lang.org"));
let info = ConnectionInfo::new(&req);
assert_eq!(info.scheme(), "http");
assert_eq!(info.host(), "rust-lang.org");
assert_eq!(info.remote(), None);
let mut req = HttpRequest::default();
req.headers_mut().insert(
HeaderName::from_str(X_FORWARDED_FOR).unwrap(),
HeaderValue::from_static("192.0.2.60"),
);
let info = ConnectionInfo::new(&req);
assert_eq!(info.remote(), Some("192.0.2.60"));
let mut req = HttpRequest::default();
req.headers_mut().insert(
HeaderName::from_str(X_FORWARDED_HOST).unwrap(),
HeaderValue::from_static("192.0.2.60"),
);
let info = ConnectionInfo::new(&req);
assert_eq!(info.host(), "192.0.2.60");
assert_eq!(info.remote(), None);
let mut req = HttpRequest::default();
req.headers_mut().insert(
HeaderName::from_str(X_FORWARDED_PROTO).unwrap(),
HeaderValue::from_static("https"),
);
let info = ConnectionInfo::new(&req);
assert_eq!(info.scheme(), "https");
}
}

431
src/json.rs Normal file
View File

@ -0,0 +1,431 @@
use bytes::{Bytes, BytesMut};
use futures::{Future, Poll, Stream};
use http::header::CONTENT_LENGTH;
use std::fmt;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use mime;
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json;
use error::{Error, JsonPayloadError, PayloadError};
use handler::{FromRequest, Responder};
use http::StatusCode;
use httpmessage::HttpMessage;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
/// Json helper
///
/// Json can be used for two different purpose. First is for json response
/// generation and second is for extracting typed information from request's
/// payload.
///
/// To extract typed information from request's body, the type `T` must
/// implement the `Deserialize` trait from *serde*.
///
/// [**JsonConfig**](dev/struct.JsonConfig.html) allows to configure extraction
/// process.
///
/// ## Example
///
/// ```rust
/// # extern crate actix_web;
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{App, Json, Result, http};
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// deserialize `Info` from request's body
/// fn index(info: Json<Info>) -> Result<String> {
/// Ok(format!("Welcome {}!", info.username))
/// }
///
/// fn main() {
/// let app = App::new().resource(
/// "/index.html",
/// |r| r.method(http::Method::POST).with(index)); // <- use `with` extractor
/// }
/// ```
///
/// The `Json` type allows you to respond with well-formed JSON data: simply
/// return a value of type Json<T> where T is the type of a structure
/// to serialize into *JSON*. The type `T` must implement the `Serialize`
/// trait from *serde*.
///
/// ```rust
/// # extern crate actix_web;
/// # #[macro_use] extern crate serde_derive;
/// # use actix_web::*;
/// #
/// #[derive(Serialize)]
/// struct MyObj {
/// name: String,
/// }
///
/// fn index(req: HttpRequest) -> Result<Json<MyObj>> {
/// Ok(Json(MyObj{name: req.match_info().query("name")?}))
/// }
/// # fn main() {}
/// ```
pub struct Json<T>(pub T);
impl<T> Json<T> {
/// Deconstruct to an inner value
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> Deref for Json<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> DerefMut for Json<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T> fmt::Debug for Json<T>
where
T: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Json: {:?}", self.0)
}
}
impl<T> fmt::Display for Json<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl<T: Serialize> Responder for Json<T> {
type Item = HttpResponse;
type Error = Error;
fn respond_to<S>(self, req: &HttpRequest<S>) -> Result<HttpResponse, Error> {
let body = serde_json::to_string(&self.0)?;
Ok(req
.build_response(StatusCode::OK)
.content_type("application/json")
.body(body))
}
}
impl<T, S> FromRequest<S> for Json<T>
where
T: DeserializeOwned + 'static,
S: 'static,
{
type Config = JsonConfig<S>;
type Result = Box<Future<Item = Self, Error = Error>>;
#[inline]
fn from_request(req: &HttpRequest<S>, cfg: &Self::Config) -> Self::Result {
let req = req.clone();
let err = Rc::clone(&cfg.ehandler);
Box::new(
JsonBody::new(req.clone())
.limit(cfg.limit)
.map_err(move |e| (*err)(e, req))
.map(Json),
)
}
}
/// Json extractor configuration
///
/// ```rust
/// # extern crate actix_web;
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{App, Json, HttpResponse, Result, http, error};
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// deserialize `Info` from request's body, max payload size is 4kb
/// fn index(info: Json<Info>) -> Result<String> {
/// Ok(format!("Welcome {}!", info.username))
/// }
///
/// fn main() {
/// let app = App::new().resource(
/// "/index.html", |r| {
/// r.method(http::Method::POST)
/// .with(index)
/// .limit(4096) // <- change json extractor configuration
/// .error_handler(|err, req| { // <- create custom error response
/// error::InternalError::from_response(
/// err, HttpResponse::Conflict().finish()).into()
/// });
/// });
/// }
/// ```
pub struct JsonConfig<S> {
limit: usize,
ehandler: Rc<Fn(JsonPayloadError, HttpRequest<S>) -> Error>,
}
impl<S> JsonConfig<S> {
/// Change max size of payload. By default max size is 256Kb
pub fn limit(&mut self, limit: usize) -> &mut Self {
self.limit = limit;
self
}
/// Set custom error handler
pub fn error_handler<F>(&mut self, f: F) -> &mut Self
where
F: Fn(JsonPayloadError, HttpRequest<S>) -> Error + 'static,
{
self.ehandler = Rc::new(f);
self
}
}
impl<S> Default for JsonConfig<S> {
fn default() -> Self {
JsonConfig {
limit: 262_144,
ehandler: Rc::new(|e, _| e.into()),
}
}
}
/// Request payload json parser that resolves to a deserialized `T` value.
///
/// Returns error:
///
/// * content type is not `application/json`
/// * content length is greater than 256k
///
/// # Server example
///
/// ```rust
/// # extern crate actix_web;
/// # extern crate futures;
/// # #[macro_use] extern crate serde_derive;
/// use futures::future::Future;
/// use actix_web::{AsyncResponder, HttpRequest, HttpResponse, HttpMessage, Error};
///
/// #[derive(Deserialize, Debug)]
/// struct MyObj {
/// name: String,
/// }
///
/// fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
/// req.json() // <- get JsonBody future
/// .from_err()
/// .and_then(|val: MyObj| { // <- deserialized value
/// println!("==== BODY ==== {:?}", val);
/// Ok(HttpResponse::Ok().into())
/// }).responder()
/// }
/// # fn main() {}
/// ```
pub struct JsonBody<T, U: DeserializeOwned> {
limit: usize,
req: Option<T>,
fut: Option<Box<Future<Item = U, Error = JsonPayloadError>>>,
}
impl<T, U: DeserializeOwned> JsonBody<T, U> {
/// Create `JsonBody` for request.
pub fn new(req: T) -> Self {
JsonBody {
limit: 262_144,
req: Some(req),
fut: None,
}
}
/// Change max size of payload. By default max size is 256Kb
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
}
impl<T, U: DeserializeOwned + 'static> Future for JsonBody<T, U>
where
T: HttpMessage + Stream<Item = Bytes, Error = PayloadError> + 'static,
{
type Item = U;
type Error = JsonPayloadError;
fn poll(&mut self) -> Poll<U, JsonPayloadError> {
if let Some(req) = self.req.take() {
if let Some(len) = req.headers().get(CONTENT_LENGTH) {
if let Ok(s) = len.to_str() {
if let Ok(len) = s.parse::<usize>() {
if len > self.limit {
return Err(JsonPayloadError::Overflow);
}
} else {
return Err(JsonPayloadError::Overflow);
}
}
}
// check content-type
let json = if let Ok(Some(mime)) = req.mime_type() {
mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON)
} else {
false
};
if !json {
return Err(JsonPayloadError::ContentType);
}
let limit = self.limit;
let fut = req
.from_err()
.fold(BytesMut::new(), move |mut body, chunk| {
if (body.len() + chunk.len()) > limit {
Err(JsonPayloadError::Overflow)
} else {
body.extend_from_slice(&chunk);
Ok(body)
}
})
.and_then(|body| Ok(serde_json::from_slice::<U>(&body)?));
self.fut = Some(Box::new(fut));
}
self.fut
.as_mut()
.expect("JsonBody could not be used second time")
.poll()
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use futures::Async;
use http::header;
use handler::Handler;
use with::{ExtractorConfig, With};
impl PartialEq for JsonPayloadError {
fn eq(&self, other: &JsonPayloadError) -> bool {
match *self {
JsonPayloadError::Overflow => match *other {
JsonPayloadError::Overflow => true,
_ => false,
},
JsonPayloadError::ContentType => match *other {
JsonPayloadError::ContentType => true,
_ => false,
},
_ => false,
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct MyObject {
name: String,
}
#[test]
fn test_json() {
let json = Json(MyObject {
name: "test".to_owned(),
});
let resp = json.respond_to(&HttpRequest::default()).unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/json"
);
}
#[test]
fn test_json_body() {
let req = HttpRequest::default();
let mut json = req.json::<MyObject>();
assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType);
let mut req = HttpRequest::default();
req.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/text"),
);
let mut json = req.json::<MyObject>();
assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType);
let mut req = HttpRequest::default();
req.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
req.headers_mut().insert(
header::CONTENT_LENGTH,
header::HeaderValue::from_static("10000"),
);
let mut json = req.json::<MyObject>().limit(100);
assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow);
let mut req = HttpRequest::default();
req.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
req.headers_mut().insert(
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
);
req.payload_mut()
.unread_data(Bytes::from_static(b"{\"name\": \"test\"}"));
let mut json = req.json::<MyObject>();
assert_eq!(
json.poll().ok().unwrap(),
Async::Ready(MyObject {
name: "test".to_owned()
})
);
}
#[test]
fn test_with_json() {
let mut cfg = ExtractorConfig::<_, Json<MyObject>>::default();
cfg.limit(4096);
let mut handler = With::new(|data: Json<MyObject>| data, cfg);
let req = HttpRequest::default();
assert!(handler.handle(req).as_err().is_some());
let mut req = HttpRequest::default();
req.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
req.headers_mut().insert(
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
);
req.payload_mut()
.unread_data(Bytes::from_static(b"{\"name\": \"test\"}"));
assert!(handler.handle(req).as_err().is_none())
}
}

View File

@ -1,66 +1,255 @@
//! Web framework for [Actix](https://github.com/actix/actix)
//! Actix web is a small, pragmatic, and extremely fast web framework
//! for Rust.
//!
//! ```rust
//! use actix_web::{server, App, Path};
//! # use std::thread;
//!
//! fn index(info: Path<(String, u32)>) -> String {
//! format!("Hello {}! id:{}", info.0, info.1)
//! }
//!
//! fn main() {
//! # thread::spawn(|| {
//! server::new(|| {
//! App::new().resource("/{name}/{id}/index.html", |r| r.with(index))
//! }).bind("127.0.0.1:8080")
//! .unwrap()
//! .run();
//! # });
//! }
//! ```
//!
//! ## Documentation & community resources
//!
//! Besides the API documentation (which you are currently looking
//! at!), several other resources are available:
//!
//! * [User Guide](https://actix.rs/docs/)
//! * [Chat on gitter](https://gitter.im/actix/actix)
//! * [GitHub repository](https://github.com/actix/actix-web)
//! * [Cargo package](https://crates.io/crates/actix-web)
//!
//! To get started navigating the API documentation you may want to
//! consider looking at the following pages:
//!
//! * [App](struct.App.html): This struct represents an actix-web
//! application and is used to configure routes and other common
//! settings.
//!
//! * [HttpServer](server/struct.HttpServer.html): This struct
//! represents an HTTP server instance and is used to instantiate and
//! configure servers.
//!
//! * [HttpRequest](struct.HttpRequest.html) and
//! [HttpResponse](struct.HttpResponse.html): These structs
//! represent HTTP requests and responses and expose various methods
//! for inspecting, creating and otherwise utilizing them.
//!
//! ## Features
//!
//! * Supported *HTTP/1.x* and *HTTP/2.0* protocols
//! * Streaming and pipelining
//! * Keep-alive and slow requests handling
//! * `WebSockets` server/client
//! * Transparent content compression/decompression (br, gzip, deflate)
//! * Configurable request routing
//! * Graceful server shutdown
//! * Multipart streams
//! * SSL support with OpenSSL or `native-tls`
//! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`)
//! * Built on top of [Actix actor framework](https://github.com/actix/actix)
//! * Supported Rust version: 1.24 or later
//!
//! ## Package feature
//!
//! * `tls` - enables ssl support via `native-tls` crate
//! * `alpn` - enables ssl support via `openssl` crate, require for `http/2`
//! support
//! * `session` - enables session support, includes `ring` crate as
//! dependency
//! * `brotli` - enables `brotli` compression support, requires `c`
//! compiler
//! * `flate-c` - enables `gzip`, `deflate` compression support, requires
//! `c` compiler
//! * `flate-rust` - experimental rust based implementation for
//! `gzip`, `deflate` compression.
//!
#![cfg_attr(actix_nightly, feature(
specialization, // for impl ErrorResponse for std::error::Error
extern_prelude,
))]
#![cfg_attr(
feature = "cargo-clippy",
allow(decimal_literal_representation, suspicious_arithmetic_impl)
)]
#[macro_use]
extern crate log;
extern crate time;
extern crate base64;
extern crate byteorder;
extern crate bytes;
extern crate sha1;
extern crate regex;
extern crate sha1;
extern crate time;
#[macro_use]
extern crate bitflags;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate futures;
extern crate tokio_io;
extern crate tokio_core;
extern crate cookie;
extern crate http;
extern crate httparse;
extern crate futures_cpupool;
extern crate http as modhttp;
extern crate http_range;
extern crate httparse;
extern crate language_tags;
extern crate libc;
extern crate mime;
extern crate mime_guess;
extern crate mio;
extern crate net2;
extern crate rand;
extern crate slab;
extern crate tokio_core;
extern crate tokio_io;
extern crate url;
#[macro_use]
extern crate serde;
#[cfg(feature = "brotli")]
extern crate brotli2;
extern crate encoding;
#[cfg(feature = "flate2")]
extern crate flate2;
extern crate h2 as http2;
extern crate num_cpus;
extern crate percent_encoding;
extern crate serde_json;
extern crate serde_urlencoded;
extern crate smallvec;
#[macro_use]
extern crate actix;
#[cfg(test)]
#[macro_use]
extern crate serde_derive;
#[cfg(feature = "tls")]
extern crate native_tls;
#[cfg(feature = "tls")]
extern crate tokio_tls;
#[cfg(feature = "openssl")]
extern crate openssl;
#[cfg(feature = "openssl")]
extern crate tokio_openssl;
mod application;
mod body;
mod context;
mod error;
mod date;
mod decode;
mod de;
mod extractor;
mod handler;
mod header;
mod helpers;
mod httpcodes;
mod httpmessage;
mod httprequest;
mod httpresponse;
mod logger;
mod info;
mod json;
mod param;
mod payload;
mod pipeline;
mod resource;
mod recognizer;
mod route;
mod reader;
mod task;
mod staticfiles;
mod server;
mod wsframe;
mod wsproto;
mod router;
mod scope;
mod uri;
mod with;
pub mod ws;
pub mod dev;
pub mod httpcodes;
pub mod client;
pub mod error;
pub mod fs;
pub mod middleware;
pub mod multipart;
pub use error::ParseError;
pub use body::{Body, BinaryBody};
pub use application::{Application, ApplicationBuilder, Middleware};
pub use httprequest::{HttpRequest, UrlEncoded};
pub use httpresponse::{HttpResponse, HttpResponseBuilder};
pub use payload::{Payload, PayloadItem, PayloadError};
pub use route::{Route, RouteFactory, RouteHandler, RouteResult};
pub use resource::{Reply, Resource, HandlerResult};
pub use recognizer::{Params, RouteRecognizer};
pub use logger::Logger;
pub use server::HttpServer;
pub mod pred;
pub mod server;
pub mod test;
pub mod ws;
pub use application::App;
pub use body::{Binary, Body};
pub use context::HttpContext;
pub use staticfiles::StaticFiles;
pub use error::{Error, ResponseError, Result};
pub use extractor::{Form, Path, Query};
pub use handler::{
AsyncResponder, Either, FromRequest, FutureResponse, Responder, State,
};
pub use httpmessage::HttpMessage;
pub use httprequest::HttpRequest;
pub use httpresponse::HttpResponse;
pub use json::Json;
pub use scope::Scope;
// re-exports
pub use http::{Method, StatusCode, Version};
pub use cookie::{Cookie, CookieBuilder};
pub use cookie::{ParseError as CookieParseError};
pub use http_range::{HttpRange, HttpRangeParseError};
#[doc(hidden)]
#[deprecated(since = "0.6.2", note = "please use `use actix_web::ws::WsWriter`")]
pub use ws::WsWriter;
#[cfg(feature = "openssl")]
pub(crate) const HAS_OPENSSL: bool = true;
#[cfg(not(feature = "openssl"))]
pub(crate) const HAS_OPENSSL: bool = false;
#[cfg(feature = "tls")]
pub(crate) const HAS_TLS: bool = true;
#[cfg(not(feature = "tls"))]
pub(crate) const HAS_TLS: bool = false;
pub mod dev {
//! The `actix-web` prelude for library developers
//!
//! The purpose of this module is to alleviate imports of many common actix
//! traits by adding a glob import to the top of actix heavy modules:
//!
//! ```
//! # #![allow(unused_imports)]
//! use actix_web::dev::*;
//! ```
pub use body::BodyStream;
pub use context::Drain;
pub use extractor::{FormConfig, PayloadConfig};
pub use handler::{AsyncResult, Handler};
pub use httpmessage::{MessageBody, UrlEncoded};
pub use httpresponse::HttpResponseBuilder;
pub use info::ConnectionInfo;
pub use json::{JsonBody, JsonConfig};
pub use param::{FromParam, Params};
pub use resource::ResourceHandler;
pub use route::Route;
pub use router::{Resource, ResourceType, Router};
pub use with::ExtractorConfig;
}
pub mod http {
//! Various HTTP related types
// re-exports
pub use modhttp::{Method, StatusCode, Version};
#[doc(hidden)]
pub use modhttp::{uri, Error, Extensions, HeaderMap, HttpTryFrom, Uri};
pub use cookie::{Cookie, CookieBuilder};
pub use http_range::HttpRange;
pub use helpers::NormalizePath;
pub mod header {
pub use header::*;
}
pub use header::ContentEncoding;
pub use httpresponse::ConnectionType;
}

View File

@ -1,279 +0,0 @@
//! Request logging middleware
use std::fmt;
use std::str::Chars;
use std::iter::Peekable;
use std::fmt::{Display, Formatter};
use time;
use application::Middleware;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
/// `Middleware` for logging request and response info to the terminal.
pub struct Logger {
format: Format,
}
impl Logger {
/// Create `Logger` middlewares with the specified `format`.
/// If a `None` is passed in, uses the default format:
///
/// ```ignore
/// {method} {uri} -> {status} ({response-time} ms)
/// ```
///
/// ```rust,ignore
/// let app = Application::default("/")
/// .middleware(Logger::new(None))
/// .finish()
/// ```
pub fn new(format: Option<Format>) -> Logger {
let format = format.unwrap_or_default();
Logger { format: format.clone() }
}
}
struct StartTime(time::Tm);
impl Logger {
fn initialise(&self, req: &mut HttpRequest) {
req.extensions().insert(StartTime(time::now()));
}
fn log(&self, req: &mut HttpRequest, resp: &HttpResponse) {
let entry_time = req.extensions().get::<StartTime>().unwrap().0;
let response_time = time::now() - entry_time;
let response_time_ms = (response_time.num_seconds() * 1000) as f64 + (response_time.num_nanoseconds().unwrap_or(0) as f64) / 1000000.0;
{
let render = |fmt: &mut Formatter, text: &FormatText| {
match *text {
FormatText::Str(ref string) => fmt.write_str(string),
FormatText::Method => req.method().fmt(fmt),
FormatText::URI => {
if req.query_string().is_empty() {
fmt.write_fmt(format_args!("{}", req.path()))
} else {
fmt.write_fmt(format_args!("{}?{}", req.path(), req.query_string()))
}
},
FormatText::Status => resp.status().fmt(fmt),
FormatText::ResponseTime =>
fmt.write_fmt(format_args!("{} ms", response_time_ms)),
FormatText::RemoteAddr => Ok(()), //req.remote_addr.fmt(fmt),
FormatText::RequestTime => {
entry_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ%z")
.unwrap()
.fmt(fmt)
}
}
};
info!("{}", self.format.display_with(&render));
}
}
}
impl Middleware for Logger {
fn start(&self, req: &mut HttpRequest) -> Result<(), HttpResponse> {
self.initialise(req);
Ok(())
}
fn finish(&self, req: &mut HttpRequest, resp: &HttpResponse) {
self.log(req, resp);
}
}
use self::FormatText::{Method, URI, Status, ResponseTime, RemoteAddr, RequestTime};
/// A formatting style for the `Logger`, consisting of multiple
/// `FormatText`s concatenated into one line.
#[derive(Clone)]
#[doc(hidden)]
pub struct Format(Vec<FormatText>);
impl Default for Format {
/// Return the default formatting style for the `Logger`:
///
/// ```ignore
/// {method} {uri} -> {status} ({response-time})
/// // This will be written as: {method} {uri} -> {status} ({response-time})
/// ```
fn default() -> Format {
Format::new("{method} {uri} {status} ({response-time})").unwrap()
}
}
impl Format {
/// Create a `Format` from a format string, which can contain the fields
/// `{method}`, `{uri}`, `{status}`, `{response-time}`, `{ip-addr}` and
/// `{request-time}`.
///
/// Returns `None` if the format string syntax is incorrect.
pub fn new(s: &str) -> Option<Format> {
let parser = FormatParser::new(s.chars().peekable());
let mut results = Vec::new();
for unit in parser {
match unit {
Some(unit) => results.push(unit),
None => return None
}
}
Some(Format(results))
}
}
pub(crate) trait ContextDisplay<'a> {
type Item;
type Display: fmt::Display;
fn display_with(&'a self,
render: &'a Fn(&mut Formatter, &Self::Item) -> Result<(), fmt::Error>)
-> Self::Display;
}
impl<'a> ContextDisplay<'a> for Format {
type Item = FormatText;
type Display = FormatDisplay<'a>;
fn display_with(&'a self,
render: &'a Fn(&mut Formatter, &FormatText) -> Result<(), fmt::Error>)
-> FormatDisplay<'a> {
FormatDisplay {
format: self,
render: render,
}
}
}
struct FormatParser<'a> {
// The characters of the format string.
chars: Peekable<Chars<'a>>,
// A reusable buffer for parsing style attributes.
object_buffer: String,
finished: bool
}
impl<'a> FormatParser<'a> {
fn new(chars: Peekable<Chars>) -> FormatParser {
FormatParser {
chars: chars,
// No attributes are longer than 14 characters, so we can avoid reallocating.
object_buffer: String::with_capacity(14),
finished: false
}
}
}
// Some(None) means there was a parse error and this FormatParser should be abandoned.
impl<'a> Iterator for FormatParser<'a> {
type Item = Option<FormatText>;
fn next(&mut self) -> Option<Option<FormatText>> {
// If the parser has been cancelled or errored for some reason.
if self.finished { return None }
// Try to parse a new FormatText.
match self.chars.next() {
// Parse a recognized object.
//
// The allowed forms are:
// - {method}
// - {uri}
// - {status}
// - {response-time}
// - {ip-addr}
// - {request-time}
Some('{') => {
self.object_buffer.clear();
let mut chr = self.chars.next();
while chr != None {
match chr.unwrap() {
// Finished parsing, parse buffer.
'}' => break,
c => self.object_buffer.push(c)
}
chr = self.chars.next();
}
let text = match self.object_buffer.as_ref() {
"method" => Method,
"uri" => URI,
"status" => Status,
"response-time" => ResponseTime,
"request-time" => RequestTime,
"ip-addr" => RemoteAddr,
_ => {
// Error, so mark as finished.
self.finished = true;
return Some(None);
}
};
Some(Some(text))
}
// Parse a regular string part of the format string.
Some(c) => {
let mut buffer = String::new();
buffer.push(c);
loop {
match self.chars.peek() {
// Done parsing.
Some(&'{') | None => return Some(Some(FormatText::Str(buffer))),
Some(_) => {
buffer.push(self.chars.next().unwrap())
}
}
}
},
// Reached end of the format string.
None => None
}
}
}
/// A string of text to be logged. This is either one of the data
/// fields supported by the `Logger`, or a custom `String`.
#[derive(Clone)]
#[doc(hidden)]
pub enum FormatText {
Str(String),
Method,
URI,
Status,
ResponseTime,
RemoteAddr,
RequestTime
}
pub(crate) struct FormatDisplay<'a> {
format: &'a Format,
render: &'a Fn(&mut Formatter, &FormatText) -> Result<(), fmt::Error>,
}
impl<'a> fmt::Display for FormatDisplay<'a> {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
let Format(ref format) = *self.format;
for unit in format {
(self.render)(fmt, unit)?;
}
Ok(())
}
}

1159
src/middleware/cors.rs Normal file

File diff suppressed because it is too large Load Diff

277
src/middleware/csrf.rs Normal file
View File

@ -0,0 +1,277 @@
//! A filter for cross-site request forgery (CSRF).
//!
//! This middleware is stateless and [based on request
//! headers](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers).
//!
//! By default requests are allowed only if one of these is true:
//!
//! * The request method is safe (`GET`, `HEAD`, `OPTIONS`). It is the
//! applications responsibility to ensure these methods cannot be used to
//! execute unwanted actions. Note that upgrade requests for websockets are
//! also considered safe.
//! * The `Origin` header (added automatically by the browser) matches one
//! of the allowed origins.
//! * There is no `Origin` header but the `Referer` header matches one of
//! the allowed origins.
//!
//! Use [`CsrfFilter::allow_xhr()`](struct.CsrfFilter.html#method.allow_xhr)
//! if you want to allow requests with unsafe methods via
//! [CORS](../cors/struct.Cors.html).
//!
//! # Example
//!
//! ```
//! # extern crate actix_web;
//! use actix_web::{http, App, HttpRequest, HttpResponse};
//! use actix_web::middleware::csrf;
//!
//! fn handle_post(_: HttpRequest) -> &'static str {
//! "This action should only be triggered with requests from the same site"
//! }
//!
//! fn main() {
//! let app = App::new()
//! .middleware(
//! csrf::CsrfFilter::new()
//! .allowed_origin("https://www.example.com"))
//! .resource("/", |r| {
//! r.method(http::Method::GET).f(|_| HttpResponse::Ok());
//! r.method(http::Method::POST).f(handle_post);
//! })
//! .finish();
//! }
//! ```
//!
//! In this example the entire application is protected from CSRF.
use std::borrow::Cow;
use std::collections::HashSet;
use bytes::Bytes;
use error::{ResponseError, Result};
use http::{header, HeaderMap, HttpTryFrom, Uri};
use httpmessage::HttpMessage;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
use middleware::{Middleware, Started};
/// Potential cross-site request forgery detected.
#[derive(Debug, Fail)]
pub enum CsrfError {
/// The HTTP request header `Origin` was required but not provided.
#[fail(display = "Origin header required")]
MissingOrigin,
/// The HTTP request header `Origin` could not be parsed correctly.
#[fail(display = "Could not parse Origin header")]
BadOrigin,
/// The cross-site request was denied.
#[fail(display = "Cross-site request denied")]
CsrDenied,
}
impl ResponseError for CsrfError {
fn error_response(&self) -> HttpResponse {
HttpResponse::Forbidden().body(self.to_string())
}
}
fn uri_origin(uri: &Uri) -> Option<String> {
match (uri.scheme_part(), uri.host(), uri.port()) {
(Some(scheme), Some(host), Some(port)) => {
Some(format!("{}://{}:{}", scheme, host, port))
}
(Some(scheme), Some(host), None) => Some(format!("{}://{}", scheme, host)),
_ => None,
}
}
fn origin(headers: &HeaderMap) -> Option<Result<Cow<str>, CsrfError>> {
headers
.get(header::ORIGIN)
.map(|origin| {
origin
.to_str()
.map_err(|_| CsrfError::BadOrigin)
.map(|o| o.into())
})
.or_else(|| {
headers.get(header::REFERER).map(|referer| {
Uri::try_from(Bytes::from(referer.as_bytes()))
.ok()
.as_ref()
.and_then(uri_origin)
.ok_or(CsrfError::BadOrigin)
.map(|o| o.into())
})
})
}
/// A middleware that filters cross-site requests.
///
/// To construct a CSRF filter:
///
/// 1. Call [`CsrfFilter::build`](struct.CsrfFilter.html#method.build) to
/// start building.
/// 2. [Add](struct.CsrfFilterBuilder.html#method.allowed_origin) allowed
/// origins.
/// 3. Call [finish](struct.CsrfFilterBuilder.html#method.finish) to retrieve
/// the constructed filter.
///
/// # Example
///
/// ```
/// use actix_web::App;
/// use actix_web::middleware::csrf;
///
/// # fn main() {
/// let app = App::new().middleware(
/// csrf::CsrfFilter::new()
/// .allowed_origin("https://www.example.com"));
/// # }
/// ```
#[derive(Default)]
pub struct CsrfFilter {
origins: HashSet<String>,
allow_xhr: bool,
allow_missing_origin: bool,
allow_upgrade: bool,
}
impl CsrfFilter {
/// Start building a `CsrfFilter`.
pub fn new() -> CsrfFilter {
CsrfFilter {
origins: HashSet::new(),
allow_xhr: false,
allow_missing_origin: false,
allow_upgrade: false,
}
}
/// Add an origin that is allowed to make requests. Will be verified
/// against the `Origin` request header.
pub fn allowed_origin<T: Into<String>>(mut self, origin: T) -> CsrfFilter {
self.origins.insert(origin.into());
self
}
/// Allow all requests with an `X-Requested-With` header.
///
/// A cross-site attacker should not be able to send requests with custom
/// headers unless a CORS policy whitelists them. Therefore it should be
/// safe to allow requests with an `X-Requested-With` header (added
/// automatically by many JavaScript libraries).
///
/// This is disabled by default, because in Safari it is possible to
/// circumvent this using redirects and Flash.
///
/// Use this method to enable more lax filtering.
pub fn allow_xhr(mut self) -> CsrfFilter {
self.allow_xhr = true;
self
}
/// Allow requests if the expected `Origin` header is missing (and
/// there is no `Referer` to fall back on).
///
/// The filter is conservative by default, but it should be safe to allow
/// missing `Origin` headers because a cross-site attacker cannot prevent
/// the browser from sending `Origin` on unsafe requests.
pub fn allow_missing_origin(mut self) -> CsrfFilter {
self.allow_missing_origin = true;
self
}
/// Allow cross-site upgrade requests (for example to open a WebSocket).
pub fn allow_upgrade(mut self) -> CsrfFilter {
self.allow_upgrade = true;
self
}
fn validate<S>(&self, req: &mut HttpRequest<S>) -> Result<(), CsrfError> {
let is_upgrade = req.headers().contains_key(header::UPGRADE);
let is_safe = req.method().is_safe() && (self.allow_upgrade || !is_upgrade);
if is_safe || (self.allow_xhr && req.headers().contains_key("x-requested-with"))
{
Ok(())
} else if let Some(header) = origin(req.headers()) {
match header {
Ok(ref origin) if self.origins.contains(origin.as_ref()) => Ok(()),
Ok(_) => Err(CsrfError::CsrDenied),
Err(err) => Err(err),
}
} else if self.allow_missing_origin {
Ok(())
} else {
Err(CsrfError::MissingOrigin)
}
}
}
impl<S> Middleware<S> for CsrfFilter {
fn start(&self, req: &mut HttpRequest<S>) -> Result<Started> {
self.validate(req)?;
Ok(Started::Done)
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::Method;
use test::TestRequest;
#[test]
fn test_safe() {
let csrf = CsrfFilter::new().allowed_origin("https://www.example.com");
let mut req = TestRequest::with_header("Origin", "https://www.w3.org")
.method(Method::HEAD)
.finish();
assert!(csrf.start(&mut req).is_ok());
}
#[test]
fn test_csrf() {
let csrf = CsrfFilter::new().allowed_origin("https://www.example.com");
let mut req = TestRequest::with_header("Origin", "https://www.w3.org")
.method(Method::POST)
.finish();
assert!(csrf.start(&mut req).is_err());
}
#[test]
fn test_referer() {
let csrf = CsrfFilter::new().allowed_origin("https://www.example.com");
let mut req = TestRequest::with_header(
"Referer",
"https://www.example.com/some/path?query=param",
).method(Method::POST)
.finish();
assert!(csrf.start(&mut req).is_ok());
}
#[test]
fn test_upgrade() {
let strict_csrf = CsrfFilter::new().allowed_origin("https://www.example.com");
let lax_csrf = CsrfFilter::new()
.allowed_origin("https://www.example.com")
.allow_upgrade();
let mut req = TestRequest::with_header("Origin", "https://cswsh.com")
.header("Connection", "Upgrade")
.header("Upgrade", "websocket")
.method(Method::GET)
.finish();
assert!(strict_csrf.start(&mut req).is_err());
assert!(lax_csrf.start(&mut req).is_ok());
}
}

View File

@ -0,0 +1,122 @@
//! Default response headers
use http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
use http::{HeaderMap, HttpTryFrom};
use error::Result;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
use middleware::{Middleware, Response};
/// `Middleware` for setting default response headers.
///
/// This middleware does not set header if response headers already contains it.
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::{http, middleware, App, HttpResponse};
///
/// fn main() {
/// let app = App::new()
/// .middleware(
/// middleware::DefaultHeaders::new()
/// .header("X-Version", "0.2"))
/// .resource("/test", |r| {
/// r.method(http::Method::GET).f(|_| HttpResponse::Ok());
/// r.method(http::Method::HEAD).f(|_| HttpResponse::MethodNotAllowed());
/// })
/// .finish();
/// }
/// ```
pub struct DefaultHeaders {
ct: bool,
headers: HeaderMap,
}
impl Default for DefaultHeaders {
fn default() -> Self {
DefaultHeaders {
ct: false,
headers: HeaderMap::new(),
}
}
}
impl DefaultHeaders {
/// Construct `DefaultHeaders` middleware.
pub fn new() -> DefaultHeaders {
DefaultHeaders::default()
}
/// Set a header.
#[inline]
#[cfg_attr(feature = "cargo-clippy", allow(match_wild_err_arm))]
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: HttpTryFrom<K>,
HeaderValue: HttpTryFrom<V>,
{
match HeaderName::try_from(key) {
Ok(key) => match HeaderValue::try_from(value) {
Ok(value) => {
self.headers.append(key, value);
}
Err(_) => panic!("Can not create header value"),
},
Err(_) => panic!("Can not create header name"),
}
self
}
/// Set *CONTENT-TYPE* header if response does not contain this header.
pub fn content_type(mut self) -> Self {
self.ct = true;
self
}
}
impl<S> Middleware<S> for DefaultHeaders {
fn response(
&self, _: &mut HttpRequest<S>, mut resp: HttpResponse,
) -> Result<Response> {
for (key, value) in self.headers.iter() {
if !resp.headers().contains_key(key) {
resp.headers_mut().insert(key, value.clone());
}
}
// default content-type
if self.ct && !resp.headers().contains_key(CONTENT_TYPE) {
resp.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"),
);
}
Ok(Response::Done(resp))
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::header::CONTENT_TYPE;
#[test]
fn test_default_headers() {
let mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001");
let mut req = HttpRequest::default();
let resp = HttpResponse::Ok().finish();
let resp = match mw.response(&mut req, resp) {
Ok(Response::Done(resp)) => resp,
_ => panic!(),
};
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
let resp = HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish();
let resp = match mw.response(&mut req, resp) {
Ok(Response::Done(resp)) => resp,
_ => panic!(),
};
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002");
}
}

View File

@ -0,0 +1,114 @@
use std::collections::HashMap;
use error::Result;
use http::StatusCode;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
use middleware::{Middleware, Response};
type ErrorHandler<S> = Fn(&mut HttpRequest<S>, HttpResponse) -> Result<Response>;
/// `Middleware` for allowing custom handlers for responses.
///
/// You can use `ErrorHandlers::handler()` method to register a custom error
/// handler for specific status code. You can modify existing response or
/// create completely new one.
///
/// ## Example
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::{http, App, HttpRequest, HttpResponse, Result};
/// use actix_web::middleware::{Response, ErrorHandlers};
///
/// fn render_500<S>(_: &mut HttpRequest<S>, resp: HttpResponse) -> Result<Response> {
/// let mut builder = resp.into_builder();
/// builder.header(http::header::CONTENT_TYPE, "application/json");
/// Ok(Response::Done(builder.into()))
/// }
///
/// fn main() {
/// let app = App::new()
/// .middleware(
/// ErrorHandlers::new()
/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500))
/// .resource("/test", |r| {
/// r.method(http::Method::GET).f(|_| HttpResponse::Ok());
/// r.method(http::Method::HEAD).f(|_| HttpResponse::MethodNotAllowed());
/// })
/// .finish();
/// }
/// ```
pub struct ErrorHandlers<S> {
handlers: HashMap<StatusCode, Box<ErrorHandler<S>>>,
}
impl<S> Default for ErrorHandlers<S> {
fn default() -> Self {
ErrorHandlers {
handlers: HashMap::new(),
}
}
}
impl<S> ErrorHandlers<S> {
/// Construct new `ErrorHandlers` instance
pub fn new() -> Self {
ErrorHandlers::default()
}
/// Register error handler for specified status code
pub fn handler<F>(mut self, status: StatusCode, handler: F) -> Self
where
F: Fn(&mut HttpRequest<S>, HttpResponse) -> Result<Response> + 'static,
{
self.handlers.insert(status, Box::new(handler));
self
}
}
impl<S: 'static> Middleware<S> for ErrorHandlers<S> {
fn response(
&self, req: &mut HttpRequest<S>, resp: HttpResponse,
) -> Result<Response> {
if let Some(handler) = self.handlers.get(&resp.status()) {
handler(req, resp)
} else {
Ok(Response::Done(resp))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::header::CONTENT_TYPE;
use http::StatusCode;
fn render_500<S>(_: &mut HttpRequest<S>, resp: HttpResponse) -> Result<Response> {
let mut builder = resp.into_builder();
builder.header(CONTENT_TYPE, "0001");
Ok(Response::Done(builder.into()))
}
#[test]
fn test_handler() {
let mw =
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500);
let mut req = HttpRequest::default();
let resp = HttpResponse::InternalServerError().finish();
let resp = match mw.response(&mut req, resp) {
Ok(Response::Done(resp)) => resp,
_ => panic!(),
};
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
let resp = HttpResponse::Ok().finish();
let resp = match mw.response(&mut req, resp) {
Ok(Response::Done(resp)) => resp,
_ => panic!(),
};
assert!(!resp.headers().contains_key(CONTENT_TYPE));
}
}

390
src/middleware/identity.rs Normal file
View File

@ -0,0 +1,390 @@
//! Request identity service for Actix applications.
//!
//! [**IdentityService**](struct.IdentityService.html) middleware can be
//! used with different policies types to store identity information.
//!
//! Bu default, only cookie identity policy is implemented. Other backend
//! implementations can be added separately.
//!
//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html)
//! uses cookies as identity storage.
//!
//! To access current request identity
//! [**RequestIdentity**](trait.RequestIdentity.html) should be used.
//! *HttpRequest* implements *RequestIdentity* trait.
//!
//! ```rust
//! use actix_web::middleware::identity::RequestIdentity;
//! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService};
//! use actix_web::*;
//!
//! fn index(req: HttpRequest) -> Result<String> {
//! // access request identity
//! if let Some(id) = req.identity() {
//! Ok(format!("Welcome! {}", id))
//! } else {
//! Ok("Welcome Anonymous!".to_owned())
//! }
//! }
//!
//! fn login(mut req: HttpRequest) -> HttpResponse {
//! req.remember("User1".to_owned()); // <- remember identity
//! HttpResponse::Ok().finish()
//! }
//!
//! fn logout(mut req: HttpRequest) -> HttpResponse {
//! req.forget(); // <- remove identity
//! HttpResponse::Ok().finish()
//! }
//!
//! fn main() {
//! let app = App::new().middleware(IdentityService::new(
//! // <- create identity middleware
//! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend
//! .name("auth-cookie")
//! .secure(false),
//! ));
//! }
//! ```
use std::rc::Rc;
use cookie::{Cookie, CookieJar, Key};
use futures::future::{err as FutErr, ok as FutOk, FutureResult};
use futures::Future;
use time::Duration;
use error::{Error, Result};
use http::header::{self, HeaderValue};
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
use middleware::{Middleware, Response, Started};
/// The helper trait to obtain your identity from a request.
///
/// ```rust
/// use actix_web::*;
/// use actix_web::middleware::identity::RequestIdentity;
///
/// fn index(req: HttpRequest) -> Result<String> {
/// // access request identity
/// if let Some(id) = req.identity() {
/// Ok(format!("Welcome! {}", id))
/// } else {
/// Ok("Welcome Anonymous!".to_owned())
/// }
/// }
///
/// fn login(mut req: HttpRequest) -> HttpResponse {
/// req.remember("User1".to_owned()); // <- remember identity
/// HttpResponse::Ok().finish()
/// }
///
/// fn logout(mut req: HttpRequest) -> HttpResponse {
/// req.forget(); // <- remove identity
/// HttpResponse::Ok().finish()
/// }
/// # fn main() {}
/// ```
pub trait RequestIdentity {
/// Return the claimed identity of the user associated request or
/// ``None`` if no identity can be found associated with the request.
fn identity(&self) -> Option<&str>;
/// Remember identity.
fn remember(&mut self, identity: String);
/// This method is used to 'forget' the current identity on subsequent
/// requests.
fn forget(&mut self);
}
impl<S> RequestIdentity for HttpRequest<S> {
fn identity(&self) -> Option<&str> {
if let Some(id) = self.extensions().get::<IdentityBox>() {
return id.0.identity();
}
None
}
fn remember(&mut self, identity: String) {
if let Some(id) = self.extensions_mut().get_mut::<IdentityBox>() {
return id.0.remember(identity);
}
}
fn forget(&mut self) {
if let Some(id) = self.extensions_mut().get_mut::<IdentityBox>() {
return id.0.forget();
}
}
}
/// An identity
pub trait Identity: 'static {
fn identity(&self) -> Option<&str>;
fn remember(&mut self, key: String);
fn forget(&mut self);
/// Write session to storage backend.
fn write(&mut self, resp: HttpResponse) -> Result<Response>;
}
/// Identity policy definition.
pub trait IdentityPolicy<S>: Sized + 'static {
type Identity: Identity;
type Future: Future<Item = Self::Identity, Error = Error>;
/// Parse the session from request and load data from a service identity.
fn from_request(&self, request: &mut HttpRequest<S>) -> Self::Future;
}
/// Request identity middleware
///
/// ```rust
/// # extern crate actix;
/// # extern crate actix_web;
/// use actix_web::App;
/// use actix_web::middleware::identity::{IdentityService, CookieIdentityPolicy};
///
/// fn main() {
/// let app = App::new().middleware(
/// IdentityService::new( // <- create identity middleware
/// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend
/// .name("auth-cookie")
/// .secure(false))
/// );
/// }
/// ```
pub struct IdentityService<T> {
backend: T,
}
impl<T> IdentityService<T> {
/// Create new identity service with specified backend.
pub fn new(backend: T) -> Self {
IdentityService { backend }
}
}
struct IdentityBox(Box<Identity>);
#[doc(hidden)]
unsafe impl Send for IdentityBox {}
#[doc(hidden)]
unsafe impl Sync for IdentityBox {}
impl<S: 'static, T: IdentityPolicy<S>> Middleware<S> for IdentityService<T> {
fn start(&self, req: &mut HttpRequest<S>) -> Result<Started> {
let mut req = req.clone();
let fut = self
.backend
.from_request(&mut req)
.then(move |res| match res {
Ok(id) => {
req.extensions_mut().insert(IdentityBox(Box::new(id)));
FutOk(None)
}
Err(err) => FutErr(err),
});
Ok(Started::Future(Box::new(fut)))
}
fn response(
&self, req: &mut HttpRequest<S>, resp: HttpResponse,
) -> Result<Response> {
if let Some(mut id) = req.extensions_mut().remove::<IdentityBox>() {
id.0.write(resp)
} else {
Ok(Response::Done(resp))
}
}
}
#[doc(hidden)]
/// Identity that uses private cookies as identity storage.
pub struct CookieIdentity {
changed: bool,
identity: Option<String>,
inner: Rc<CookieIdentityInner>,
}
impl Identity for CookieIdentity {
fn identity(&self) -> Option<&str> {
self.identity.as_ref().map(|s| s.as_ref())
}
fn remember(&mut self, value: String) {
self.changed = true;
self.identity = Some(value);
}
fn forget(&mut self) {
self.changed = true;
self.identity = None;
}
fn write(&mut self, mut resp: HttpResponse) -> Result<Response> {
if self.changed {
let _ = self.inner.set_cookie(&mut resp, self.identity.take());
}
Ok(Response::Done(resp))
}
}
struct CookieIdentityInner {
key: Key,
name: String,
path: String,
domain: Option<String>,
secure: bool,
max_age: Option<Duration>,
}
impl CookieIdentityInner {
fn new(key: &[u8]) -> CookieIdentityInner {
CookieIdentityInner {
key: Key::from_master(key),
name: "actix-identity".to_owned(),
path: "/".to_owned(),
domain: None,
secure: true,
max_age: None,
}
}
fn set_cookie(&self, resp: &mut HttpResponse, id: Option<String>) -> Result<()> {
let some = id.is_some();
{
let id = id.unwrap_or_else(String::new);
let mut cookie = Cookie::new(self.name.clone(), id);
cookie.set_path(self.path.clone());
cookie.set_secure(self.secure);
cookie.set_http_only(true);
if let Some(ref domain) = self.domain {
cookie.set_domain(domain.clone());
}
if let Some(max_age) = self.max_age {
cookie.set_max_age(max_age);
}
let mut jar = CookieJar::new();
if some {
jar.private(&self.key).add(cookie);
} else {
jar.add_original(cookie.clone());
jar.private(&self.key).remove(cookie);
}
for cookie in jar.delta() {
let val = HeaderValue::from_str(&cookie.to_string())?;
resp.headers_mut().append(header::SET_COOKIE, val);
}
}
Ok(())
}
fn load<S>(&self, req: &mut HttpRequest<S>) -> Option<String> {
if let Ok(cookies) = req.cookies() {
for cookie in cookies {
if cookie.name() == self.name {
let mut jar = CookieJar::new();
jar.add_original(cookie.clone());
let cookie_opt = jar.private(&self.key).get(&self.name);
if let Some(cookie) = cookie_opt {
return Some(cookie.value().into());
}
}
}
}
None
}
}
/// Use cookies for request identity storage.
///
/// The constructors take a key as an argument.
/// This is the private key for cookie - when this value is changed,
/// all identities are lost. The constructors will panic if the key is less
/// than 32 bytes in length.
///
/// # Example
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::App;
/// use actix_web::middleware::identity::{IdentityService, CookieIdentityPolicy};
///
/// fn main() {
/// let app = App::new().middleware(
/// IdentityService::new( // <- create identity middleware
/// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy
/// .domain("www.rust-lang.org")
/// .name("actix_auth")
/// .path("/")
/// .secure(true)));
/// }
/// ```
pub struct CookieIdentityPolicy(Rc<CookieIdentityInner>);
impl CookieIdentityPolicy {
/// Construct new `CookieIdentityPolicy` instance.
///
/// Panics if key length is less than 32 bytes.
pub fn new(key: &[u8]) -> CookieIdentityPolicy {
CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key)))
}
/// Sets the `path` field in the session cookie being built.
pub fn path<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().path = value.into();
self
}
/// Sets the `name` field in the session cookie being built.
pub fn name<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().name = value.into();
self
}
/// Sets the `domain` field in the session cookie being built.
pub fn domain<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into());
self
}
/// Sets the `secure` field in the session cookie being built.
///
/// If the `secure` field is set, a cookie will only be transmitted when the
/// connection is secure - i.e. `https`
pub fn secure(mut self, value: bool) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().secure = value;
self
}
/// Sets the `max-age` field in the session cookie being built.
pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().max_age = Some(value);
self
}
}
impl<S> IdentityPolicy<S> for CookieIdentityPolicy {
type Identity = CookieIdentity;
type Future = FutureResult<CookieIdentity, Error>;
fn from_request(&self, req: &mut HttpRequest<S>) -> Self::Future {
let identity = self.0.load(req);
FutOk(CookieIdentity {
identity,
changed: false,
inner: Rc::clone(&self.0),
})
}
}

412
src/middleware/logger.rs Normal file
View File

@ -0,0 +1,412 @@
//! Request logging middleware
use std::collections::HashSet;
use std::env;
use std::fmt::{self, Display, Formatter};
use libc;
use regex::Regex;
use time;
use error::Result;
use httpmessage::HttpMessage;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
use middleware::{Finished, Middleware, Started};
/// `Middleware` for logging request and response info to the terminal.
///
/// `Logger` middleware uses standard log crate to log information. You should
/// enable logger for `actix_web` package to see access log.
/// ([`env_logger`](https://docs.rs/env_logger/*/env_logger/) or similar)
///
/// ## Usage
///
/// Create `Logger` middleware with the specified `format`.
/// Default `Logger` could be created with `default` method, it uses the
/// default format:
///
/// ```ignore
/// %a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
/// ```
/// ```rust
/// # extern crate actix_web;
/// extern crate env_logger;
/// use actix_web::App;
/// use actix_web::middleware::Logger;
///
/// fn main() {
/// std::env::set_var("RUST_LOG", "actix_web=info");
/// env_logger::init();
///
/// let app = App::new()
/// .middleware(Logger::default())
/// .middleware(Logger::new("%a %{User-Agent}i"))
/// .finish();
/// }
/// ```
///
/// ## Format
///
/// `%%` The percent sign
///
/// `%a` Remote IP-address (IP-address of proxy if using reverse proxy)
///
/// `%t` Time when the request was started to process
///
/// `%P` The process ID of the child that serviced the request
///
/// `%r` First line of request
///
/// `%s` Response status code
///
/// `%b` Size of response in bytes, including HTTP headers
///
/// `%T` Time taken to serve the request, in seconds with floating fraction in
/// .06f format
///
/// `%D` Time taken to serve the request, in milliseconds
///
/// `%{FOO}i` request.headers['FOO']
///
/// `%{FOO}o` response.headers['FOO']
///
/// `%{FOO}e` os.environ['FOO']
///
pub struct Logger {
format: Format,
exclude: HashSet<String>,
}
impl Logger {
/// Create `Logger` middleware with the specified `format`.
pub fn new(format: &str) -> Logger {
Logger {
format: Format::new(format),
exclude: HashSet::new(),
}
}
/// Ignore and do not log access info for specified path.
pub fn exclude<T: Into<String>>(mut self, path: T) -> Self {
self.exclude.insert(path.into());
self
}
}
impl Default for Logger {
/// Create `Logger` middleware with format:
///
/// ```ignore
/// %a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
/// ```
fn default() -> Logger {
Logger {
format: Format::default(),
exclude: HashSet::new(),
}
}
}
struct StartTime(time::Tm);
impl Logger {
fn log<S>(&self, req: &mut HttpRequest<S>, resp: &HttpResponse) {
if let Some(entry_time) = req.extensions().get::<StartTime>() {
let render = |fmt: &mut Formatter| {
for unit in &self.format.0 {
unit.render(fmt, req, resp, entry_time.0)?;
}
Ok(())
};
info!("{}", FormatDisplay(&render));
}
}
}
impl<S> Middleware<S> for Logger {
fn start(&self, req: &mut HttpRequest<S>) -> Result<Started> {
if !self.exclude.contains(req.path()) {
req.extensions_mut().insert(StartTime(time::now()));
}
Ok(Started::Done)
}
fn finish(&self, req: &mut HttpRequest<S>, resp: &HttpResponse) -> Finished {
self.log(req, resp);
Finished::Done
}
}
/// A formatting style for the `Logger`, consisting of multiple
/// `FormatText`s concatenated into one line.
#[derive(Clone)]
#[doc(hidden)]
struct Format(Vec<FormatText>);
impl Default for Format {
/// Return the default formatting style for the `Logger`:
fn default() -> Format {
Format::new(r#"%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#)
}
}
impl Format {
/// Create a `Format` from a format string.
///
/// Returns `None` if the format string syntax is incorrect.
pub fn new(s: &str) -> Format {
trace!("Access log format: {}", s);
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap();
let mut idx = 0;
let mut results = Vec::new();
for cap in fmt.captures_iter(s) {
let m = cap.get(0).unwrap();
let pos = m.start();
if idx != pos {
results.push(FormatText::Str(s[idx..pos].to_owned()));
}
idx = m.end();
if let Some(key) = cap.get(2) {
results.push(match cap.get(3).unwrap().as_str() {
"i" => FormatText::RequestHeader(key.as_str().to_owned()),
"o" => FormatText::ResponseHeader(key.as_str().to_owned()),
"e" => FormatText::EnvironHeader(key.as_str().to_owned()),
_ => unreachable!(),
})
} else {
let m = cap.get(1).unwrap();
results.push(match m.as_str() {
"%" => FormatText::Percent,
"a" => FormatText::RemoteAddr,
"t" => FormatText::RequestTime,
"P" => FormatText::Pid,
"r" => FormatText::RequestLine,
"s" => FormatText::ResponseStatus,
"b" => FormatText::ResponseSize,
"T" => FormatText::Time,
"D" => FormatText::TimeMillis,
_ => FormatText::Str(m.as_str().to_owned()),
});
}
}
if idx != s.len() {
results.push(FormatText::Str(s[idx..].to_owned()));
}
Format(results)
}
}
/// A string of text to be logged. This is either one of the data
/// fields supported by the `Logger`, or a custom `String`.
#[doc(hidden)]
#[derive(Debug, Clone)]
pub enum FormatText {
Str(String),
Pid,
Percent,
RequestLine,
RequestTime,
ResponseStatus,
ResponseSize,
Time,
TimeMillis,
RemoteAddr,
RequestHeader(String),
ResponseHeader(String),
EnvironHeader(String),
}
impl FormatText {
fn render<S>(
&self, fmt: &mut Formatter, req: &HttpRequest<S>, resp: &HttpResponse,
entry_time: time::Tm,
) -> Result<(), fmt::Error> {
match *self {
FormatText::Str(ref string) => fmt.write_str(string),
FormatText::Percent => "%".fmt(fmt),
FormatText::RequestLine => {
if req.query_string().is_empty() {
fmt.write_fmt(format_args!(
"{} {} {:?}",
req.method(),
req.path(),
req.version()
))
} else {
fmt.write_fmt(format_args!(
"{} {}?{} {:?}",
req.method(),
req.path(),
req.query_string(),
req.version()
))
}
}
FormatText::ResponseStatus => resp.status().as_u16().fmt(fmt),
FormatText::ResponseSize => resp.response_size().fmt(fmt),
FormatText::Pid => unsafe { libc::getpid().fmt(fmt) },
FormatText::Time => {
let rt = time::now() - entry_time;
let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0;
fmt.write_fmt(format_args!("{:.6}", rt))
}
FormatText::TimeMillis => {
let rt = time::now() - entry_time;
let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000.0;
fmt.write_fmt(format_args!("{:.6}", rt))
}
FormatText::RemoteAddr => {
if let Some(remote) = req.connection_info().remote() {
return remote.fmt(fmt);
} else {
"-".fmt(fmt)
}
}
FormatText::RequestTime => entry_time
.strftime("[%d/%b/%Y:%H:%M:%S %z]")
.unwrap()
.fmt(fmt),
FormatText::RequestHeader(ref name) => {
let s = if let Some(val) = req.headers().get(name) {
if let Ok(s) = val.to_str() {
s
} else {
"-"
}
} else {
"-"
};
fmt.write_fmt(format_args!("{}", s))
}
FormatText::ResponseHeader(ref name) => {
let s = if let Some(val) = resp.headers().get(name) {
if let Ok(s) = val.to_str() {
s
} else {
"-"
}
} else {
"-"
};
fmt.write_fmt(format_args!("{}", s))
}
FormatText::EnvironHeader(ref name) => {
if let Ok(val) = env::var(name) {
fmt.write_fmt(format_args!("{}", val))
} else {
"-".fmt(fmt)
}
}
}
}
}
pub(crate) struct FormatDisplay<'a>(&'a Fn(&mut Formatter) -> Result<(), fmt::Error>);
impl<'a> fmt::Display for FormatDisplay<'a> {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
(self.0)(fmt)
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::header::{self, HeaderMap};
use http::{Method, StatusCode, Uri, Version};
use std::str::FromStr;
use time;
#[test]
fn test_logger() {
let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test");
let mut headers = HeaderMap::new();
headers.insert(
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
);
let mut req = HttpRequest::new(
Method::GET,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
headers,
None,
);
let resp = HttpResponse::build(StatusCode::OK)
.header("X-Test", "ttt")
.force_close()
.finish();
match logger.start(&mut req) {
Ok(Started::Done) => (),
_ => panic!(),
};
match logger.finish(&mut req, &resp) {
Finished::Done => (),
_ => panic!(),
}
let entry_time = time::now();
let render = |fmt: &mut Formatter| {
for unit in &logger.format.0 {
unit.render(fmt, &req, &resp, entry_time)?;
}
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
assert!(s.contains("ACTIX-WEB ttt"));
}
#[test]
fn test_default_format() {
let format = Format::default();
let mut headers = HeaderMap::new();
headers.insert(
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
);
let req = HttpRequest::new(
Method::GET,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
headers,
None,
);
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
let entry_time = time::now();
let render = |fmt: &mut Formatter| {
for unit in &format.0 {
unit.render(fmt, &req, &resp, entry_time)?;
}
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
assert!(s.contains("GET / HTTP/1.1"));
assert!(s.contains("200 0"));
assert!(s.contains("ACTIX-WEB"));
let req = HttpRequest::new(
Method::GET,
Uri::from_str("/?test").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
let entry_time = time::now();
let render = |fmt: &mut Formatter| {
for unit in &format.0 {
unit.render(fmt, &req, &resp, entry_time)?;
}
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
assert!(s.contains("GET /?test HTTP/1.1"));
}
}

70
src/middleware/mod.rs Normal file
View File

@ -0,0 +1,70 @@
//! Middlewares
use futures::Future;
use error::{Error, Result};
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
mod logger;
pub mod cors;
pub mod csrf;
mod defaultheaders;
mod errhandlers;
#[cfg(feature = "session")]
pub mod identity;
#[cfg(feature = "session")]
pub mod session;
pub use self::defaultheaders::DefaultHeaders;
pub use self::errhandlers::ErrorHandlers;
pub use self::logger::Logger;
/// Middleware start result
pub enum Started {
/// Middleware is completed, continue to next middleware
Done,
/// New http response got generated. If middleware generates response
/// handler execution halts.
Response(HttpResponse),
/// Execution completed, runs future to completion.
Future(Box<Future<Item = Option<HttpResponse>, Error = Error>>),
}
/// Middleware execution result
pub enum Response {
/// New http response got generated
Done(HttpResponse),
/// Result is a future that resolves to a new http response
Future(Box<Future<Item = HttpResponse, Error = Error>>),
}
/// Middleware finish result
pub enum Finished {
/// Execution completed
Done,
/// Execution completed, but run future to completion
Future(Box<Future<Item = (), Error = Error>>),
}
/// Middleware definition
#[allow(unused_variables)]
pub trait Middleware<S>: 'static {
/// Method is called when request is ready. It may return
/// future, which should resolve before next middleware get called.
fn start(&self, req: &mut HttpRequest<S>) -> Result<Started> {
Ok(Started::Done)
}
/// Method is called when handler returns response,
/// but before sending http message to peer.
fn response(
&self, req: &mut HttpRequest<S>, resp: HttpResponse,
) -> Result<Response> {
Ok(Response::Done(resp))
}
/// Method is called after body stream get sent to peer.
fn finish(&self, req: &mut HttpRequest<S>, resp: &HttpResponse) -> Finished {
Finished::Done
}
}

600
src/middleware/session.rs Normal file
View File

@ -0,0 +1,600 @@
//! User sessions.
//!
//! Actix provides a general solution for session management. The
//! [**SessionStorage**](struct.SessionStorage.html)
//! middleware can be used with different backend types to store session
//! data in different backends.
//!
//! By default, only cookie session backend is implemented. Other
//! backend implementations can be added.
//!
//! [**CookieSessionBackend**](struct.CookieSessionBackend.html)
//! uses cookies as session storage. `CookieSessionBackend` creates sessions
//! which are limited to storing fewer than 4000 bytes of data, as the payload
//! must fit into a single cookie. An internal server error is generated if a
//! session contains more than 4000 bytes.
//!
//! A cookie may have a security policy of *signed* or *private*. Each has
//! a respective `CookieSessionBackend` constructor.
//!
//! A *signed* cookie may be viewed but not modified by the client. A *private*
//! cookie may neither be viewed nor modified by the client.
//!
//! The constructors take a key as an argument. This is the private key
//! for cookie session - when this value is changed, all session data is lost.
//!
//! In general, you create a `SessionStorage` middleware and initialize it
//! with specific backend implementation, such as a `CookieSessionBackend`.
//! To access session data,
//! [*HttpRequest::session()*](trait.RequestSession.html#tymethod.session)
//! must be used. This method returns a
//! [*Session*](struct.Session.html) object, which allows us to get or set
//! session data.
//!
//! ```rust
//! # extern crate actix;
//! # extern crate actix_web;
//! use actix_web::{server, App, HttpRequest, Result};
//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend};
//!
//! fn index(req: HttpRequest) -> Result<&'static str> {
//! // access session data
//! if let Some(count) = req.session().get::<i32>("counter")? {
//! println!("SESSION value: {}", count);
//! req.session().set("counter", count+1)?;
//! } else {
//! req.session().set("counter", 1)?;
//! }
//!
//! Ok("Welcome!")
//! }
//!
//! fn main() {
//! let sys = actix::System::new("basic-example");
//! server::new(
//! || App::new().middleware(
//! SessionStorage::new( // <- create session middleware
//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend
//! .secure(false)
//! )))
//! .bind("127.0.0.1:59880").unwrap()
//! .start();
//! # actix::Arbiter::system().do_send(actix::msgs::SystemExit(0));
//! let _ = sys.run();
//! }
//! ```
use std::cell::RefCell;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::Arc;
use cookie::{Cookie, CookieJar, Key};
use futures::future::{err as FutErr, ok as FutOk, FutureResult};
use futures::Future;
use http::header::{self, HeaderValue};
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json;
use serde_json::error::Error as JsonError;
use time::Duration;
use error::{Error, ResponseError, Result};
use handler::FromRequest;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
use middleware::{Middleware, Response, Started};
/// The helper trait to obtain your session data from a request.
///
/// ```rust
/// use actix_web::*;
/// use actix_web::middleware::session::RequestSession;
///
/// fn index(mut req: HttpRequest) -> Result<&'static str> {
/// // access session data
/// if let Some(count) = req.session().get::<i32>("counter")? {
/// req.session().set("counter", count+1)?;
/// } else {
/// req.session().set("counter", 1)?;
/// }
///
/// Ok("Welcome!")
/// }
/// # fn main() {}
/// ```
pub trait RequestSession {
fn session(&self) -> Session;
}
impl<S> RequestSession for HttpRequest<S> {
fn session(&self) -> Session {
if let Some(s_impl) = self.extensions().get::<Arc<SessionImplCell>>() {
return Session(SessionInner::Session(Arc::clone(&s_impl)));
}
Session(SessionInner::None)
}
}
/// The high-level interface you use to modify session data.
///
/// Session object could be obtained with
/// [`RequestSession::session`](trait.RequestSession.html#tymethod.session)
/// method. `RequestSession` trait is implemented for `HttpRequest`.
///
/// ```rust
/// use actix_web::*;
/// use actix_web::middleware::session::RequestSession;
///
/// fn index(mut req: HttpRequest) -> Result<&'static str> {
/// // access session data
/// if let Some(count) = req.session().get::<i32>("counter")? {
/// req.session().set("counter", count+1)?;
/// } else {
/// req.session().set("counter", 1)?;
/// }
///
/// Ok("Welcome!")
/// }
/// # fn main() {}
/// ```
pub struct Session(SessionInner);
enum SessionInner {
Session(Arc<SessionImplCell>),
None,
}
impl Session {
/// Get a `value` from the session.
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
match self.0 {
SessionInner::Session(ref sess) => {
if let Some(s) = sess.as_ref().0.borrow().get(key) {
Ok(Some(serde_json::from_str(s)?))
} else {
Ok(None)
}
}
SessionInner::None => Ok(None),
}
}
/// Set a `value` from the session.
pub fn set<T: Serialize>(&self, key: &str, value: T) -> Result<()> {
match self.0 {
SessionInner::Session(ref sess) => {
sess.as_ref()
.0
.borrow_mut()
.set(key, serde_json::to_string(&value)?);
Ok(())
}
SessionInner::None => Ok(()),
}
}
/// Remove value from the session.
pub fn remove(&self, key: &str) {
match self.0 {
SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().remove(key),
SessionInner::None => (),
}
}
/// Clear the session.
pub fn clear(&self) {
match self.0 {
SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().clear(),
SessionInner::None => (),
}
}
}
/// Extractor implementation for Session type.
///
/// ```rust
/// # use actix_web::*;
/// use actix_web::middleware::session::Session;
///
/// fn index(session: Session) -> Result<&'static str> {
/// // access session data
/// if let Some(count) = session.get::<i32>("counter")? {
/// session.set("counter", count+1)?;
/// } else {
/// session.set("counter", 1)?;
/// }
///
/// Ok("Welcome!")
/// }
/// # fn main() {}
/// ```
impl<S> FromRequest<S> for Session {
type Config = ();
type Result = Session;
#[inline]
fn from_request(req: &HttpRequest<S>, _: &Self::Config) -> Self::Result {
req.session()
}
}
struct SessionImplCell(RefCell<Box<SessionImpl>>);
#[doc(hidden)]
unsafe impl Send for SessionImplCell {}
#[doc(hidden)]
unsafe impl Sync for SessionImplCell {}
/// Session storage middleware
///
/// ```rust
/// # extern crate actix;
/// # extern crate actix_web;
/// use actix_web::App;
/// use actix_web::middleware::session::{SessionStorage, CookieSessionBackend};
///
/// fn main() {
/// let app = App::new().middleware(
/// SessionStorage::new( // <- create session middleware
/// CookieSessionBackend::signed(&[0; 32]) // <- create cookie session backend
/// .secure(false))
/// );
/// }
/// ```
pub struct SessionStorage<T, S>(T, PhantomData<S>);
impl<S, T: SessionBackend<S>> SessionStorage<T, S> {
/// Create session storage
pub fn new(backend: T) -> SessionStorage<T, S> {
SessionStorage(backend, PhantomData)
}
}
impl<S: 'static, T: SessionBackend<S>> Middleware<S> for SessionStorage<T, S> {
fn start(&self, req: &mut HttpRequest<S>) -> Result<Started> {
let mut req = req.clone();
let fut = self.0.from_request(&mut req).then(move |res| match res {
Ok(sess) => {
req.extensions_mut()
.insert(Arc::new(SessionImplCell(RefCell::new(Box::new(sess)))));
FutOk(None)
}
Err(err) => FutErr(err),
});
Ok(Started::Future(Box::new(fut)))
}
fn response(
&self, req: &mut HttpRequest<S>, resp: HttpResponse,
) -> Result<Response> {
if let Some(s_box) = req.extensions_mut().remove::<Arc<SessionImplCell>>() {
s_box.0.borrow_mut().write(resp)
} else {
Ok(Response::Done(resp))
}
}
}
/// A simple key-value storage interface that is internally used by `Session`.
#[doc(hidden)]
pub trait SessionImpl: 'static {
fn get(&self, key: &str) -> Option<&str>;
fn set(&mut self, key: &str, value: String);
fn remove(&mut self, key: &str);
fn clear(&mut self);
/// Write session to storage backend.
fn write(&self, resp: HttpResponse) -> Result<Response>;
}
/// Session's storage backend trait definition.
#[doc(hidden)]
pub trait SessionBackend<S>: Sized + 'static {
type Session: SessionImpl;
type ReadFuture: Future<Item = Self::Session, Error = Error>;
/// Parse the session from request and load data from a storage backend.
fn from_request(&self, request: &mut HttpRequest<S>) -> Self::ReadFuture;
}
/// Session that uses signed cookies as session storage
pub struct CookieSession {
changed: bool,
state: HashMap<String, String>,
inner: Rc<CookieSessionInner>,
}
/// Errors that can occur during handling cookie session
#[derive(Fail, Debug)]
pub enum CookieSessionError {
/// Size of the serialized session is greater than 4000 bytes.
#[fail(display = "Size of the serialized session is greater than 4000 bytes.")]
Overflow,
/// Fail to serialize session.
#[fail(display = "Fail to serialize session")]
Serialize(JsonError),
}
impl ResponseError for CookieSessionError {}
impl SessionImpl for CookieSession {
fn get(&self, key: &str) -> Option<&str> {
if let Some(s) = self.state.get(key) {
Some(s)
} else {
None
}
}
fn set(&mut self, key: &str, value: String) {
self.changed = true;
self.state.insert(key.to_owned(), value);
}
fn remove(&mut self, key: &str) {
self.changed = true;
self.state.remove(key);
}
fn clear(&mut self) {
self.changed = true;
self.state.clear()
}
fn write(&self, mut resp: HttpResponse) -> Result<Response> {
if self.changed {
let _ = self.inner.set_cookie(&mut resp, &self.state);
}
Ok(Response::Done(resp))
}
}
enum CookieSecurity {
Signed,
Private,
}
struct CookieSessionInner {
key: Key,
security: CookieSecurity,
name: String,
path: String,
domain: Option<String>,
secure: bool,
max_age: Option<Duration>,
}
impl CookieSessionInner {
fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner {
CookieSessionInner {
security,
key: Key::from_master(key),
name: "actix-session".to_owned(),
path: "/".to_owned(),
domain: None,
secure: true,
max_age: None,
}
}
fn set_cookie(
&self, resp: &mut HttpResponse, state: &HashMap<String, String>,
) -> Result<()> {
let value =
serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?;
if value.len() > 4064 {
return Err(CookieSessionError::Overflow.into());
}
let mut cookie = Cookie::new(self.name.clone(), value);
cookie.set_path(self.path.clone());
cookie.set_secure(self.secure);
cookie.set_http_only(true);
if let Some(ref domain) = self.domain {
cookie.set_domain(domain.clone());
}
if let Some(max_age) = self.max_age {
cookie.set_max_age(max_age);
}
let mut jar = CookieJar::new();
match self.security {
CookieSecurity::Signed => jar.signed(&self.key).add(cookie),
CookieSecurity::Private => jar.private(&self.key).add(cookie),
}
for cookie in jar.delta() {
let val = HeaderValue::from_str(&cookie.to_string())?;
resp.headers_mut().append(header::SET_COOKIE, val);
}
Ok(())
}
fn load<S>(&self, req: &mut HttpRequest<S>) -> HashMap<String, String> {
if let Ok(cookies) = req.cookies() {
for cookie in cookies {
if cookie.name() == self.name {
let mut jar = CookieJar::new();
jar.add_original(cookie.clone());
let cookie_opt = match self.security {
CookieSecurity::Signed => jar.signed(&self.key).get(&self.name),
CookieSecurity::Private => {
jar.private(&self.key).get(&self.name)
}
};
if let Some(cookie) = cookie_opt {
if let Ok(val) = serde_json::from_str(cookie.value()) {
return val;
}
}
}
}
}
HashMap::new()
}
}
/// Use cookies for session storage.
///
/// `CookieSessionBackend` creates sessions which are limited to storing
/// fewer than 4000 bytes of data (as the payload must fit into a single
/// cookie). An Internal Server Error is generated if the session contains more
/// than 4000 bytes.
///
/// A cookie may have a security policy of *signed* or *private*. Each has a
/// respective `CookieSessionBackend` constructor.
///
/// A *signed* cookie is stored on the client as plaintext alongside
/// a signature such that the cookie may be viewed but not modified by the
/// client.
///
/// A *private* cookie is stored on the client as encrypted text
/// such that it may neither be viewed nor modified by the client.
///
/// The constructors take a key as an argument.
/// This is the private key for cookie session - when this value is changed,
/// all session data is lost. The constructors will panic if the key is less
/// than 32 bytes in length.
///
///
/// # Example
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::middleware::session::CookieSessionBackend;
///
/// # fn main() {
/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32])
/// .domain("www.rust-lang.org")
/// .name("actix_session")
/// .path("/")
/// .secure(true);
/// # }
/// ```
pub struct CookieSessionBackend(Rc<CookieSessionInner>);
impl CookieSessionBackend {
/// Construct new *signed* `CookieSessionBackend` instance.
///
/// Panics if key length is less than 32 bytes.
pub fn signed(key: &[u8]) -> CookieSessionBackend {
CookieSessionBackend(Rc::new(CookieSessionInner::new(
key,
CookieSecurity::Signed,
)))
}
/// Construct new *private* `CookieSessionBackend` instance.
///
/// Panics if key length is less than 32 bytes.
pub fn private(key: &[u8]) -> CookieSessionBackend {
CookieSessionBackend(Rc::new(CookieSessionInner::new(
key,
CookieSecurity::Private,
)))
}
/// Sets the `path` field in the session cookie being built.
pub fn path<S: Into<String>>(mut self, value: S) -> CookieSessionBackend {
Rc::get_mut(&mut self.0).unwrap().path = value.into();
self
}
/// Sets the `name` field in the session cookie being built.
pub fn name<S: Into<String>>(mut self, value: S) -> CookieSessionBackend {
Rc::get_mut(&mut self.0).unwrap().name = value.into();
self
}
/// Sets the `domain` field in the session cookie being built.
pub fn domain<S: Into<String>>(mut self, value: S) -> CookieSessionBackend {
Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into());
self
}
/// Sets the `secure` field in the session cookie being built.
///
/// If the `secure` field is set, a cookie will only be transmitted when the
/// connection is secure - i.e. `https`
pub fn secure(mut self, value: bool) -> CookieSessionBackend {
Rc::get_mut(&mut self.0).unwrap().secure = value;
self
}
/// Sets the `max-age` field in the session cookie being built.
pub fn max_age(mut self, value: Duration) -> CookieSessionBackend {
Rc::get_mut(&mut self.0).unwrap().max_age = Some(value);
self
}
}
impl<S> SessionBackend<S> for CookieSessionBackend {
type Session = CookieSession;
type ReadFuture = FutureResult<CookieSession, Error>;
fn from_request(&self, req: &mut HttpRequest<S>) -> Self::ReadFuture {
let state = self.0.load(req);
FutOk(CookieSession {
changed: false,
inner: Rc::clone(&self.0),
state,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use application::App;
use test;
#[test]
fn cookie_session() {
let mut srv = test::TestServer::with_factory(|| {
App::new()
.middleware(SessionStorage::new(
CookieSessionBackend::signed(&[0; 32]).secure(false),
))
.resource("/", |r| {
r.f(|req| {
let _ = req.session().set("counter", 100);
"test"
})
})
});
let request = srv.get().uri(srv.url("/")).finish().unwrap();
let response = srv.execute(request.send()).unwrap();
assert!(response.cookie("actix-session").is_some());
}
#[test]
fn cookie_session_extractor() {
let mut srv = test::TestServer::with_factory(|| {
App::new()
.middleware(SessionStorage::new(
CookieSessionBackend::signed(&[0; 32]).secure(false),
))
.resource("/", |r| {
r.with(|ses: Session| {
let _ = ses.set("counter", 100);
"test"
})
})
});
let request = srv.get().uri(srv.url("/")).finish().unwrap();
let response = srv.execute(request.send()).unwrap();
assert!(response.cookie("actix-session").is_some());
}
}

View File

@ -1,107 +1,46 @@
//! Multipart requests support.
use std::{cmp, fmt};
use std::rc::Rc;
//! Multipart requests support
use std::cell::RefCell;
use std::error::Error;
use std::marker::PhantomData;
use std::rc::Rc;
use std::{cmp, fmt};
use mime;
use httparse;
use bytes::Bytes;
use http::HttpTryFrom;
use futures::task::{current as current_task, Task};
use futures::{Async, Poll, Stream};
use http::header::{self, HeaderMap, HeaderName, HeaderValue};
use futures::{Async, Stream, Poll};
use futures::task::{Task, current as current_task};
use http::HttpTryFrom;
use httparse;
use mime;
use error::ParseError;
use payload::{Payload, PayloadError};
use error::{MultipartError, ParseError, PayloadError};
use payload::PayloadHelper;
const MAX_HEADERS: usize = 32;
/// A set of errors that can occur during parsing multipart streams.
#[derive(Debug)]
pub enum MultipartError {
/// Content-Type header is not found
NoContentType,
/// Can not parse Content-Type header
ParseContentType,
/// Multipart boundary is not found
Boundary,
/// Error during field parsing
Parse(ParseError),
/// Payload error
Payload(PayloadError),
}
impl fmt::Display for MultipartError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
MultipartError::Parse(ref e) => fmt::Display::fmt(e, f),
MultipartError::Payload(ref e) => fmt::Display::fmt(e, f),
ref e => f.write_str(e.description()),
}
}
}
impl Error for MultipartError {
fn description(&self) -> &str {
match *self {
MultipartError::NoContentType => "No Content-type header found",
MultipartError::ParseContentType => "Can not parse Content-Type header",
MultipartError::Boundary => "Multipart boundary is not found",
MultipartError::Parse(ref e) => e.description(),
MultipartError::Payload(ref e) => e.description(),
}
}
fn cause(&self) -> Option<&Error> {
match *self {
MultipartError::Parse(ref error) => Some(error),
MultipartError::Payload(ref error) => Some(error),
_ => None,
}
}
}
impl From<ParseError> for MultipartError {
fn from(err: ParseError) -> MultipartError {
MultipartError::Parse(err)
}
}
impl From<PayloadError> for MultipartError {
fn from(err: PayloadError) -> MultipartError {
MultipartError::Payload(err)
}
}
/// The server-side implementation of `multipart/form-data` requests.
///
/// This will parse the incoming stream into `MultipartItem` instances via its
/// Stream implementation.
/// `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart`
/// is used for nested multipart streams.
#[derive(Debug)]
pub struct Multipart {
pub struct Multipart<S> {
safety: Safety,
inner: Rc<RefCell<InnerMultipart>>,
error: Option<MultipartError>,
inner: Option<Rc<RefCell<InnerMultipart<S>>>>,
}
///
#[derive(Debug)]
pub enum MultipartItem {
pub enum MultipartItem<S> {
/// Multipart field
Field(Field),
Field(Field<S>),
/// Nested multipart stream
Nested(Multipart),
Nested(Multipart<S>),
}
#[derive(Debug)]
enum InnerMultipartItem {
enum InnerMultipartItem<S> {
None,
Field(Rc<RefCell<InnerField>>),
Multipart(Rc<RefCell<InnerMultipart>>),
Field(Rc<RefCell<InnerField<S>>>),
Multipart(Rc<RefCell<InnerMultipart<S>>>),
}
#[derive(PartialEq, Debug)]
@ -116,29 +55,14 @@ enum InnerState {
Headers,
}
#[derive(Debug)]
struct InnerMultipart {
payload: PayloadRef,
struct InnerMultipart<S> {
payload: PayloadRef<S>,
boundary: String,
state: InnerState,
item: InnerMultipartItem,
item: InnerMultipartItem<S>,
}
impl Multipart {
/// Create multipart instance for boundary.
pub fn new(boundary: String, payload: Payload) -> Multipart {
Multipart {
safety: Safety::new(),
inner: Rc::new(RefCell::new(
InnerMultipart {
payload: PayloadRef::new(payload),
boundary: boundary,
state: InnerState::FirstBoundary,
item: InnerMultipartItem::None,
}))
}
}
impl Multipart<()> {
/// Extract boundary info from headers.
pub fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
@ -161,26 +85,59 @@ impl Multipart {
}
}
impl Stream for Multipart {
type Item = MultipartItem;
impl<S> Multipart<S>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
/// Create multipart instance for boundary.
pub fn new(boundary: Result<String, MultipartError>, stream: S) -> Multipart<S> {
match boundary {
Ok(boundary) => Multipart {
error: None,
safety: Safety::new(),
inner: Some(Rc::new(RefCell::new(InnerMultipart {
boundary,
payload: PayloadRef::new(PayloadHelper::new(stream)),
state: InnerState::FirstBoundary,
item: InnerMultipartItem::None,
}))),
},
Err(err) => Multipart {
error: Some(err),
safety: Safety::new(),
inner: None,
},
}
}
}
impl<S> Stream for Multipart<S>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
type Item = MultipartItem<S>;
type Error = MultipartError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
if self.safety.current() {
self.inner.borrow_mut().poll(&self.safety)
if let Some(err) = self.error.take() {
Err(err)
} else if self.safety.current() {
self.inner.as_mut().unwrap().borrow_mut().poll(&self.safety)
} else {
Ok(Async::NotReady)
}
}
}
impl InnerMultipart {
fn read_headers(payload: &mut Payload) -> Poll<HeaderMap, MultipartError>
{
match payload.readuntil(b"\r\n\r\n")? {
impl<S> InnerMultipart<S>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
fn read_headers(payload: &mut PayloadHelper<S>) -> Poll<HeaderMap, MultipartError> {
match payload.read_until(b"\r\n\r\n")? {
Async::NotReady => Ok(Async::NotReady),
Async::Ready(bytes) => {
Async::Ready(None) => Err(MultipartError::Incomplete),
Async::Ready(Some(bytes)) => {
let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS];
match httparse::parse_headers(&bytes, &mut hdrs) {
Ok(httparse::Status::Complete((_, hdrs))) => {
@ -191,10 +148,10 @@ impl InnerMultipart {
if let Ok(value) = HeaderValue::try_from(h.value) {
headers.append(name, value);
} else {
return Err(ParseError::Header.into())
return Err(ParseError::Header.into());
}
} else {
return Err(ParseError::Header.into())
return Err(ParseError::Header.into());
}
}
Ok(Async::Ready(headers))
@ -206,21 +163,23 @@ impl InnerMultipart {
}
}
fn read_boundary(payload: &mut Payload, boundary: &str) -> Poll<bool, MultipartError>
{
fn read_boundary(
payload: &mut PayloadHelper<S>, boundary: &str,
) -> Poll<bool, MultipartError> {
// TODO: need to read epilogue
match payload.readline()? {
Async::NotReady => Ok(Async::NotReady),
Async::Ready(chunk) => {
if chunk.len() == boundary.len() + 4 &&
&chunk[..2] == b"--" &&
&chunk[2..boundary.len()+2] == boundary.as_bytes()
Async::Ready(None) => Err(MultipartError::Incomplete),
Async::Ready(Some(chunk)) => {
if chunk.len() == boundary.len() + 4
&& &chunk[..2] == b"--"
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes()
{
Ok(Async::Ready(false))
} else if chunk.len() == boundary.len() + 6 &&
&chunk[..2] == b"--" &&
&chunk[2..boundary.len()+2] == boundary.as_bytes() &&
&chunk[boundary.len()+2..boundary.len()+4] == b"--"
} else if chunk.len() == boundary.len() + 6
&& &chunk[..2] == b"--"
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes()
&& &chunk[boundary.len() + 2..boundary.len() + 4] == b"--"
{
Ok(Async::Ready(true))
} else {
@ -230,39 +189,47 @@ impl InnerMultipart {
}
}
fn skip_until_boundary(payload: &mut Payload, boundary: &str) -> Poll<bool, MultipartError>
{
fn skip_until_boundary(
payload: &mut PayloadHelper<S>, boundary: &str,
) -> Poll<bool, MultipartError> {
let mut eof = false;
loop {
if let Async::Ready(chunk) = payload.readline()? {
if chunk.is_empty() {
//ValueError("Could not find starting boundary %r"
//% (self._boundary))
}
if chunk.len() < boundary.len() {
continue
}
if &chunk[..2] == b"--" && &chunk[2..chunk.len()-2] == boundary.as_bytes() {
break;
} else {
if chunk.len() < boundary.len() + 2{
continue
match payload.readline()? {
Async::Ready(Some(chunk)) => {
if chunk.is_empty() {
//ValueError("Could not find starting boundary %r"
//% (self._boundary))
}
let b: &[u8] = boundary.as_ref();
if &chunk[..boundary.len()] == b &&
&chunk[boundary.len()..boundary.len()+2] == b"--" {
if chunk.len() < boundary.len() {
continue;
}
if &chunk[..2] == b"--"
&& &chunk[2..chunk.len() - 2] == boundary.as_bytes()
{
break;
} else {
if chunk.len() < boundary.len() + 2 {
continue;
}
let b: &[u8] = boundary.as_ref();
if &chunk[..boundary.len()] == b
&& &chunk[boundary.len()..boundary.len() + 2] == b"--"
{
eof = true;
break;
}
}
}
} else {
return Ok(Async::NotReady)
Async::NotReady => return Ok(Async::NotReady),
Async::Ready(None) => return Err(MultipartError::Incomplete),
}
}
Ok(Async::Ready(eof))
}
fn poll(&mut self, safety: &Safety) -> Poll<Option<MultipartItem>, MultipartError> {
fn poll(
&mut self, safety: &Safety,
) -> Poll<Option<MultipartItem<S>>, MultipartError> {
if self.state == InnerState::Eof {
Ok(Async::Ready(None))
} else {
@ -274,23 +241,16 @@ impl InnerMultipart {
let stop = match self.item {
InnerMultipartItem::Field(ref mut field) => {
match field.borrow_mut().poll(safety)? {
Async::NotReady => {
return Ok(Async::NotReady)
}
Async::Ready(Some(_)) =>
continue,
Async::Ready(None) =>
true,
Async::NotReady => return Ok(Async::NotReady),
Async::Ready(Some(_)) => continue,
Async::Ready(None) => true,
}
}
InnerMultipartItem::Multipart(ref mut multipart) => {
match multipart.borrow_mut().poll(safety)? {
Async::NotReady =>
return Ok(Async::NotReady),
Async::Ready(Some(_)) =>
continue,
Async::Ready(None) =>
true,
Async::NotReady => return Ok(Async::NotReady),
Async::Ready(Some(_)) => continue,
Async::Ready(None) => true,
}
}
_ => false,
@ -308,25 +268,25 @@ impl InnerMultipart {
match self.state {
// read until first boundary
InnerState::FirstBoundary => {
if let Async::Ready(eof) =
InnerMultipart::skip_until_boundary(payload, &self.boundary)?
{
if eof {
self.state = InnerState::Eof;
return Ok(Async::Ready(None));
} else {
self.state = InnerState::Headers;
match InnerMultipart::skip_until_boundary(
payload,
&self.boundary,
)? {
Async::Ready(eof) => {
if eof {
self.state = InnerState::Eof;
return Ok(Async::Ready(None));
} else {
self.state = InnerState::Headers;
}
}
} else {
return Ok(Async::NotReady)
Async::NotReady => return Ok(Async::NotReady),
}
}
// read boundary
InnerState::Boundary => {
match InnerMultipart::read_boundary(payload, &self.boundary)? {
Async::NotReady => {
return Ok(Async::NotReady)
}
Async::NotReady => return Ok(Async::NotReady),
Async::Ready(eof) => {
if eof {
self.state = InnerState::Eof;
@ -342,18 +302,19 @@ impl InnerMultipart {
// read field headers for next field
if self.state == InnerState::Headers {
if let Async::Ready(headers) = InnerMultipart::read_headers(payload)? {
if let Async::Ready(headers) = InnerMultipart::read_headers(payload)?
{
self.state = InnerState::Boundary;
headers
} else {
return Ok(Async::NotReady)
return Ok(Async::NotReady);
}
} else {
unreachable!()
}
} else {
debug!("NotReady: field is in flight");
return Ok(Async::NotReady)
return Ok(Async::NotReady);
};
// content type
@ -371,36 +332,43 @@ impl InnerMultipart {
// nested multipart stream
if mt.type_() == mime::MULTIPART {
let inner = if let Some(boundary) = mt.get_param(mime::BOUNDARY) {
Rc::new(RefCell::new(
InnerMultipart {
payload: self.payload.clone(),
boundary: boundary.as_str().to_owned(),
state: InnerState::FirstBoundary,
item: InnerMultipartItem::None,
}))
Rc::new(RefCell::new(InnerMultipart {
payload: self.payload.clone(),
boundary: boundary.as_str().to_owned(),
state: InnerState::FirstBoundary,
item: InnerMultipartItem::None,
}))
} else {
return Err(MultipartError::Boundary)
return Err(MultipartError::Boundary);
};
self.item = InnerMultipartItem::Multipart(Rc::clone(&inner));
Ok(Async::Ready(Some(
MultipartItem::Nested(
Multipart{safety: safety.clone(), inner: inner}))))
Ok(Async::Ready(Some(MultipartItem::Nested(Multipart {
safety: safety.clone(),
error: None,
inner: Some(inner),
}))))
} else {
let field = Rc::new(RefCell::new(InnerField::new(
self.payload.clone(), self.boundary.clone(), &headers)?));
self.payload.clone(),
self.boundary.clone(),
&headers,
)?));
self.item = InnerMultipartItem::Field(Rc::clone(&field));
Ok(Async::Ready(Some(
MultipartItem::Field(
Field::new(safety.clone(), headers, mt, field)))))
Ok(Async::Ready(Some(MultipartItem::Field(Field::new(
safety.clone(),
headers,
mt,
field,
)))))
}
}
}
}
impl Drop for InnerMultipart {
impl<S> Drop for InnerMultipart<S> {
fn drop(&mut self) {
// InnerMultipartItem::Field has to be dropped first because of Safety.
self.item = InnerMultipartItem::None;
@ -408,26 +376,26 @@ impl Drop for InnerMultipart {
}
/// A single field in a multipart stream
pub struct Field {
pub struct Field<S> {
ct: mime::Mime,
headers: HeaderMap,
inner: Rc<RefCell<InnerField>>,
inner: Rc<RefCell<InnerField<S>>>,
safety: Safety,
}
/// A field's chunk
#[derive(PartialEq, Debug)]
pub struct FieldChunk(pub Bytes);
impl Field {
fn new(safety: Safety, headers: HeaderMap,
ct: mime::Mime, inner: Rc<RefCell<InnerField>>) -> Self {
impl<S> Field<S>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
fn new(
safety: Safety, headers: HeaderMap, ct: mime::Mime,
inner: Rc<RefCell<InnerField<S>>>,
) -> Self {
Field {
ct: ct,
headers: headers,
inner: inner,
safety: safety,
ct,
headers,
inner,
safety,
}
}
@ -440,8 +408,11 @@ impl Field {
}
}
impl Stream for Field {
type Item = FieldChunk;
impl<S> Stream for Field<S>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
type Item = Bytes;
type Error = MultipartError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
@ -453,100 +424,105 @@ impl Stream for Field {
}
}
impl fmt::Debug for Field {
impl<S> fmt::Debug for Field<S> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let res = write!(f, "\nMultipartField: {}\n", self.ct);
let _ = write!(f, " boundary: {}\n", self.inner.borrow().boundary);
let _ = write!(f, " headers:\n");
for key in self.headers.keys() {
let vals: Vec<_> = self.headers.get_all(key).iter().collect();
if vals.len() > 1 {
let _ = write!(f, " {:?}: {:?}\n", key, vals);
} else {
let _ = write!(f, " {:?}: {:?}\n", key, vals[0]);
}
let res = writeln!(f, "\nMultipartField: {}", self.ct);
let _ = writeln!(f, " boundary: {}", self.inner.borrow().boundary);
let _ = writeln!(f, " headers:");
for (key, val) in self.headers.iter() {
let _ = writeln!(f, " {:?}: {:?}", key, val);
}
res
}
}
#[derive(Debug)]
struct InnerField {
payload: Option<PayloadRef>,
struct InnerField<S> {
payload: Option<PayloadRef<S>>,
boundary: String,
eof: bool,
length: Option<u64>,
}
impl InnerField {
fn new(payload: PayloadRef, boundary: String, headers: &HeaderMap)
-> Result<InnerField, PayloadError>
{
impl<S> InnerField<S>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
fn new(
payload: PayloadRef<S>, boundary: String, headers: &HeaderMap,
) -> Result<InnerField<S>, PayloadError> {
let len = if let Some(len) = headers.get(header::CONTENT_LENGTH) {
if let Ok(s) = len.to_str() {
if let Ok(len) = s.parse::<u64>() {
Some(len)
} else {
return Err(PayloadError::Incomplete)
return Err(PayloadError::Incomplete);
}
} else {
return Err(PayloadError::Incomplete)
return Err(PayloadError::Incomplete);
}
} else {
None
};
Ok(InnerField {
boundary,
payload: Some(payload),
boundary: boundary,
eof: false,
length: len })
length: len,
})
}
/// Reads body part content chunk of the specified size.
/// The body part must has `Content-Length` header with proper value.
fn read_len(payload: &mut Payload, size: &mut u64) -> Poll<Option<Bytes>, MultipartError>
{
fn read_len(
payload: &mut PayloadHelper<S>, size: &mut u64,
) -> Poll<Option<Bytes>, MultipartError> {
if *size == 0 {
Ok(Async::Ready(None))
} else {
match payload.readany() {
Ok(Async::NotReady) => Ok(Async::NotReady),
Ok(Async::Ready(None)) => Ok(Async::Ready(None)),
Ok(Async::Ready(None)) => Err(MultipartError::Incomplete),
Ok(Async::Ready(Some(mut chunk))) => {
let len = cmp::min(chunk.0.len() as u64, *size);
let len = cmp::min(chunk.len() as u64, *size);
*size -= len;
let ch = chunk.0.split_to(len as usize);
if !chunk.0.is_empty() {
payload.unread_data(chunk.0);
let ch = chunk.split_to(len as usize);
if !chunk.is_empty() {
payload.unread_data(chunk);
}
Ok(Async::Ready(Some(ch)))
},
Err(err) => Err(err.into())
}
Err(err) => Err(err.into()),
}
}
}
/// Reads content chunk of body part with unknown length.
/// The `Content-Length` header for body part is not necessary.
fn read_stream(payload: &mut Payload, boundary: &str) -> Poll<Option<Bytes>, MultipartError>
{
match payload.readuntil(b"\r")? {
fn read_stream(
payload: &mut PayloadHelper<S>, boundary: &str,
) -> Poll<Option<Bytes>, MultipartError> {
match payload.read_until(b"\r")? {
Async::NotReady => Ok(Async::NotReady),
Async::Ready(mut chunk) => {
Async::Ready(None) => Err(MultipartError::Incomplete),
Async::Ready(Some(mut chunk)) => {
if chunk.len() == 1 {
payload.unread_data(chunk);
match payload.readexactly(boundary.len() + 4)? {
match payload.read_exact(boundary.len() + 4)? {
Async::NotReady => Ok(Async::NotReady),
Async::Ready(chunk) => {
if &chunk[..2] == b"\r\n" && &chunk[2..4] == b"--" &&
&chunk[4..] == boundary.as_bytes()
Async::Ready(None) => Err(MultipartError::Incomplete),
Async::Ready(Some(mut chunk)) => {
if &chunk[..2] == b"\r\n"
&& &chunk[2..4] == b"--"
&& &chunk[4..] == boundary.as_bytes()
{
payload.unread_data(chunk);
Ok(Async::Ready(None))
} else {
Ok(Async::Ready(Some(chunk)))
// \r might be part of data stream
let ch = chunk.split_to(1);
payload.unread_data(chunk);
Ok(Async::Ready(Some(ch)))
}
}
}
@ -560,27 +536,9 @@ impl InnerField {
}
}
fn poll(&mut self, s: &Safety) -> Poll<Option<FieldChunk>, MultipartError> {
fn poll(&mut self, s: &Safety) -> Poll<Option<Bytes>, MultipartError> {
if self.payload.is_none() {
return Ok(Async::Ready(None))
}
if self.eof {
if let Some(payload) = self.payload.as_ref().unwrap().get_mut(s) {
match payload.readline()? {
Async::NotReady =>
return Ok(Async::NotReady),
Async::Ready(chunk) => {
assert_eq!(
chunk.as_ref(), b"\r\n",
"reader did not read all the data or it is malformed");
}
}
} else {
return Ok(Async::NotReady);
}
self.payload.take();
return Ok(Async::Ready(None))
return Ok(Async::Ready(None));
}
let result = if let Some(payload) = self.payload.as_ref().unwrap().get_mut(s) {
@ -592,15 +550,16 @@ impl InnerField {
match res {
Async::NotReady => Async::NotReady,
Async::Ready(Some(bytes)) => Async::Ready(Some(FieldChunk(bytes))),
Async::Ready(Some(bytes)) => Async::Ready(Some(bytes)),
Async::Ready(None) => {
self.eof = true;
match payload.readline()? {
Async::NotReady => Async::NotReady,
Async::Ready(chunk) => {
assert_eq!(
chunk.as_ref(), b"\r\n",
"reader did not read all the data or it is malformed");
Async::Ready(None) => Async::Ready(None),
Async::Ready(Some(line)) => {
if line.as_ref() != b"\r\n" {
warn!("multipart field did not read all the data or it is malformed");
}
Async::Ready(None)
}
}
@ -617,26 +576,27 @@ impl InnerField {
}
}
#[derive(Debug)]
struct PayloadRef {
task: Option<Task>,
payload: Rc<Payload>,
struct PayloadRef<S> {
payload: Rc<PayloadHelper<S>>,
}
impl PayloadRef {
fn new(payload: Payload) -> PayloadRef {
impl<S> PayloadRef<S>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
fn new(payload: PayloadHelper<S>) -> PayloadRef<S> {
PayloadRef {
task: None,
payload: Rc::new(payload),
}
}
fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option<&'a mut Payload>
where 'a: 'b
fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option<&'a mut PayloadHelper<S>>
where
'a: 'b,
{
if s.current() {
let payload: &mut Payload = unsafe {
&mut *(self.payload.as_ref() as *const _ as *mut _)};
let payload: &mut PayloadHelper<S> =
unsafe { &mut *(self.payload.as_ref() as *const _ as *mut _) };
Some(payload)
} else {
None
@ -644,17 +604,17 @@ impl PayloadRef {
}
}
impl Clone for PayloadRef {
fn clone(&self) -> PayloadRef {
impl<S> Clone for PayloadRef<S> {
fn clone(&self) -> PayloadRef<S> {
PayloadRef {
task: Some(current_task()),
payload: Rc::clone(&self.payload),
}
}
}
/// Counter. It tracks of number of clones of payloads and give access to payload only
/// to top most task panics if Safety get destroyed and it not top most task.
/// Counter. It tracks of number of clones of payloads and give access to
/// payload only to top most task panics if Safety get destroyed and it not top
/// most task.
#[derive(Debug)]
struct Safety {
task: Option<Task>,
@ -668,14 +628,13 @@ impl Safety {
Safety {
task: None,
level: Rc::strong_count(&payload),
payload: payload,
payload,
}
}
fn current(&self) -> bool {
Rc::strong_count(&self.payload) == self.level
}
}
impl Clone for Safety {
@ -684,7 +643,7 @@ impl Clone for Safety {
Safety {
task: Some(current_task()),
level: Rc::strong_count(&payload),
payload: payload,
payload,
}
}
}
@ -706,51 +665,60 @@ mod tests {
use super::*;
use bytes::Bytes;
use futures::future::{lazy, result};
use payload::{Payload, PayloadWriter};
use tokio_core::reactor::Core;
use payload::Payload;
#[test]
fn test_boundary() {
let headers = HeaderMap::new();
match Multipart::boundary(&headers) {
Err(MultipartError::NoContentType) => (),
_ => panic!("should not happen"),
}
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE,
header::HeaderValue::from_static("test"));
match Multipart::boundary(&headers) {
Err(MultipartError::ParseContentType) => (),
_ => panic!("should not happen"),
_ => unreachable!("should not happen"),
}
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("multipart/mixed"));
header::HeaderValue::from_static("test"),
);
match Multipart::boundary(&headers) {
Err(MultipartError::ParseContentType) => (),
_ => unreachable!("should not happen"),
}
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("multipart/mixed"),
);
match Multipart::boundary(&headers) {
Err(MultipartError::Boundary) => (),
_ => panic!("should not happen"),
_ => unreachable!("should not happen"),
}
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\""));
"multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"",
),
);
assert_eq!(Multipart::boundary(&headers).unwrap(),
"5c02368e880e436dab70ed54e1c58209");
assert_eq!(
Multipart::boundary(&headers).unwrap(),
"5c02368e880e436dab70ed54e1c58209"
);
}
#[test]
fn test_multipart() {
Core::new().unwrap().run(lazy(|| {
let (mut sender, payload) = Payload::new(false);
Core::new()
.unwrap()
.run(lazy(|| {
let (mut sender, payload) = Payload::new(false);
let bytes = Bytes::from(
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
@ -759,64 +727,64 @@ mod tests {
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
data\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n");
sender.feed_data(bytes);
sender.feed_data(bytes);
let mut multipart = Multipart::new(
"abbc761f78ff4d7cb7573b5a23f96ef0".to_owned(), payload);
match multipart.poll() {
Ok(Async::Ready(Some(item))) => {
println!("{:?}", item);
match item {
let mut multipart = Multipart::new(
Ok("abbc761f78ff4d7cb7573b5a23f96ef0".to_owned()),
payload,
);
match multipart.poll() {
Ok(Async::Ready(Some(item))) => match item {
MultipartItem::Field(mut field) => {
assert_eq!(field.content_type().type_(), mime::TEXT);
assert_eq!(field.content_type().subtype(), mime::PLAIN);
match field.poll() {
Ok(Async::Ready(Some(chunk))) =>
assert_eq!(chunk.0, "test"),
_ => unreachable!()
Ok(Async::Ready(Some(chunk))) => {
assert_eq!(chunk, "test")
}
_ => unreachable!(),
}
match field.poll() {
Ok(Async::Ready(None)) => (),
_ => unreachable!()
_ => unreachable!(),
}
},
_ => unreachable!()
}
}
_ => unreachable!(),
},
_ => unreachable!(),
}
_ => unreachable!()
}
match multipart.poll() {
Ok(Async::Ready(Some(item))) => {
match item {
match multipart.poll() {
Ok(Async::Ready(Some(item))) => match item {
MultipartItem::Field(mut field) => {
assert_eq!(field.content_type().type_(), mime::TEXT);
assert_eq!(field.content_type().subtype(), mime::PLAIN);
match field.poll() {
Ok(Async::Ready(Some(chunk))) =>
assert_eq!(chunk.0, "data"),
_ => unreachable!()
Ok(Async::Ready(Some(chunk))) => {
assert_eq!(chunk, "data")
}
_ => unreachable!(),
}
match field.poll() {
Ok(Async::Ready(None)) => (),
_ => unreachable!()
_ => unreachable!(),
}
},
_ => unreachable!()
}
}
_ => unreachable!(),
},
_ => unreachable!(),
}
_ => unreachable!()
}
match multipart.poll() {
Ok(Async::Ready(None)) => (),
_ => unreachable!()
}
match multipart.poll() {
Ok(Async::Ready(None)) => (),
_ => unreachable!(),
}
let res: Result<(), ()> = Ok(());
result(res)
})).unwrap();
let res: Result<(), ()> = Ok(());
result(res)
}))
.unwrap();
}
}

251
src/param.rs Normal file
View File

@ -0,0 +1,251 @@
use http::StatusCode;
use smallvec::SmallVec;
use std;
use std::borrow::Cow;
use std::ops::Index;
use std::path::PathBuf;
use std::slice::Iter;
use std::str::FromStr;
use error::{InternalError, ResponseError, UriSegmentError};
/// A trait to abstract the idea of creating a new instance of a type from a
/// path parameter.
pub trait FromParam: Sized {
/// The associated error which can be returned from parsing.
type Err: ResponseError;
/// Parses a string `s` to return a value of this type.
fn from_param(s: &str) -> Result<Self, Self::Err>;
}
/// Route match information
///
/// If resource path contains variable patterns, `Params` stores this variables.
#[derive(Debug)]
pub struct Params<'a>(SmallVec<[(Cow<'a, str>, Cow<'a, str>); 3]>);
impl<'a> Params<'a> {
pub(crate) fn new() -> Params<'a> {
Params(SmallVec::new())
}
pub(crate) fn clear(&mut self) {
self.0.clear();
}
pub(crate) fn add<N, V>(&mut self, name: N, value: V)
where
N: Into<Cow<'a, str>>,
V: Into<Cow<'a, str>>,
{
self.0.push((name.into(), value.into()));
}
pub(crate) fn set<N, V>(&mut self, name: N, value: V)
where
N: Into<Cow<'a, str>>,
V: Into<Cow<'a, str>>,
{
let name = name.into();
let value = value.into();
for item in &mut self.0 {
if item.0 == name {
item.1 = value;
return;
}
}
self.0.push((name, value));
}
pub(crate) fn remove(&mut self, name: &str) {
for idx in (0..self.0.len()).rev() {
if self.0[idx].0 == name {
self.0.remove(idx);
return;
}
}
}
/// Check if there are any matched patterns
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Check number of extracted parameters
pub fn len(&self) -> usize {
self.0.len()
}
/// Get matched parameter by name without type conversion
pub fn get(&'a self, key: &str) -> Option<&'a str> {
for item in self.0.iter() {
if key == item.0 {
return Some(item.1.as_ref());
}
}
None
}
/// Get matched `FromParam` compatible parameter by name.
///
/// If keyed parameter is not available empty string is used as default
/// value.
///
/// ```rust
/// # extern crate actix_web;
/// # use actix_web::*;
/// fn index(req: HttpRequest) -> Result<String> {
/// let ivalue: isize = req.match_info().query("val")?;
/// Ok(format!("isuze value: {:?}", ivalue))
/// }
/// # fn main() {}
/// ```
pub fn query<T: FromParam>(&'a self, key: &str) -> Result<T, <T as FromParam>::Err> {
if let Some(s) = self.get(key) {
T::from_param(s)
} else {
T::from_param("")
}
}
/// Return iterator to items in parameter container
pub fn iter(&self) -> Iter<(Cow<'a, str>, Cow<'a, str>)> {
self.0.iter()
}
}
impl<'a, 'b, 'c: 'a> Index<&'b str> for &'c Params<'a> {
type Output = str;
fn index(&self, name: &'b str) -> &str {
self.get(name)
.expect("Value for parameter is not available")
}
}
impl<'a, 'c: 'a> Index<usize> for &'c Params<'a> {
type Output = str;
fn index(&self, idx: usize) -> &str {
self.0[idx].1.as_ref()
}
}
/// Creates a `PathBuf` from a path parameter. The returned `PathBuf` is
/// percent-decoded. If a segment is equal to "..", the previous segment (if
/// any) is skipped.
///
/// For security purposes, if a segment meets any of the following conditions,
/// an `Err` is returned indicating the condition met:
///
/// * Decoded segment starts with any of: `.` (except `..`), `*`
/// * Decoded segment ends with any of: `:`, `>`, `<`
/// * Decoded segment contains any of: `/`
/// * On Windows, decoded segment contains any of: '\'
/// * Percent-encoding results in invalid UTF8.
///
/// As a result of these conditions, a `PathBuf` parsed from request path
/// parameter is safe to interpolate within, or use as a suffix of, a path
/// without additional checks.
impl FromParam for PathBuf {
type Err = UriSegmentError;
fn from_param(val: &str) -> Result<PathBuf, UriSegmentError> {
let mut buf = PathBuf::new();
for segment in val.split('/') {
if segment == ".." {
buf.pop();
} else if segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.'));
} else if segment.starts_with('*') {
return Err(UriSegmentError::BadStart('*'));
} else if segment.ends_with(':') {
return Err(UriSegmentError::BadEnd(':'));
} else if segment.ends_with('>') {
return Err(UriSegmentError::BadEnd('>'));
} else if segment.ends_with('<') {
return Err(UriSegmentError::BadEnd('<'));
} else if segment.is_empty() {
continue;
} else if cfg!(windows) && segment.contains('\\') {
return Err(UriSegmentError::BadChar('\\'));
} else {
buf.push(segment)
}
}
Ok(buf)
}
}
macro_rules! FROM_STR {
($type:ty) => {
impl FromParam for $type {
type Err = InternalError<<$type as FromStr>::Err>;
fn from_param(val: &str) -> Result<Self, Self::Err> {
<$type as FromStr>::from_str(val)
.map_err(|e| InternalError::new(e, StatusCode::BAD_REQUEST))
}
}
};
}
FROM_STR!(u8);
FROM_STR!(u16);
FROM_STR!(u32);
FROM_STR!(u64);
FROM_STR!(usize);
FROM_STR!(i8);
FROM_STR!(i16);
FROM_STR!(i32);
FROM_STR!(i64);
FROM_STR!(isize);
FROM_STR!(f32);
FROM_STR!(f64);
FROM_STR!(String);
FROM_STR!(std::net::IpAddr);
FROM_STR!(std::net::Ipv4Addr);
FROM_STR!(std::net::Ipv6Addr);
FROM_STR!(std::net::SocketAddr);
FROM_STR!(std::net::SocketAddrV4);
FROM_STR!(std::net::SocketAddrV6);
#[cfg(test)]
mod tests {
use super::*;
use std::iter::FromIterator;
#[test]
fn test_path_buf() {
assert_eq!(
PathBuf::from_param("/test/.tt"),
Err(UriSegmentError::BadStart('.'))
);
assert_eq!(
PathBuf::from_param("/test/*tt"),
Err(UriSegmentError::BadStart('*'))
);
assert_eq!(
PathBuf::from_param("/test/tt:"),
Err(UriSegmentError::BadEnd(':'))
);
assert_eq!(
PathBuf::from_param("/test/tt<"),
Err(UriSegmentError::BadEnd('<'))
);
assert_eq!(
PathBuf::from_param("/test/tt>"),
Err(UriSegmentError::BadEnd('>'))
);
assert_eq!(
PathBuf::from_param("/seg1/seg2/"),
Ok(PathBuf::from_iter(vec!["seg1", "seg2"]))
);
assert_eq!(
PathBuf::from_param("/seg1/../seg2/"),
Ok(PathBuf::from_iter(vec!["seg2"]))
);
}
}

File diff suppressed because it is too large Load Diff

830
src/pipeline.rs Normal file
View File

@ -0,0 +1,830 @@
use std::cell::UnsafeCell;
use std::marker::PhantomData;
use std::rc::Rc;
use std::{io, mem};
use futures::unsync::oneshot;
use futures::{Async, Future, Poll, Stream};
use log::Level::Debug;
use application::Inner;
use body::{Body, BodyStream};
use context::{ActorHttpContext, Frame};
use error::Error;
use handler::{AsyncResult, AsyncResultItem};
use header::ContentEncoding;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
use middleware::{Finished, Middleware, Response, Started};
use server::{HttpHandlerTask, Writer, WriterState};
#[derive(Debug, Clone, Copy)]
pub(crate) enum HandlerType {
Normal(usize),
Handler(usize),
Default,
}
pub(crate) trait PipelineHandler<S> {
fn encoding(&self) -> ContentEncoding;
fn handle(
&mut self, req: HttpRequest<S>, htype: HandlerType,
) -> AsyncResult<HttpResponse>;
}
pub(crate) struct Pipeline<S, H>(PipelineInfo<S>, PipelineState<S, H>);
enum PipelineState<S, H> {
None,
Error,
Starting(StartMiddlewares<S, H>),
Handler(WaitingResponse<S, H>),
RunMiddlewares(RunMiddlewares<S, H>),
Response(ProcessResponse<S, H>),
Finishing(FinishingMiddlewares<S, H>),
Completed(Completed<S, H>),
}
impl<S: 'static, H: PipelineHandler<S>> PipelineState<S, H> {
fn is_response(&self) -> bool {
match *self {
PipelineState::Response(_) => true,
_ => false,
}
}
fn poll(&mut self, info: &mut PipelineInfo<S>) -> Option<PipelineState<S, H>> {
match *self {
PipelineState::Starting(ref mut state) => state.poll(info),
PipelineState::Handler(ref mut state) => state.poll(info),
PipelineState::RunMiddlewares(ref mut state) => state.poll(info),
PipelineState::Finishing(ref mut state) => state.poll(info),
PipelineState::Completed(ref mut state) => state.poll(info),
PipelineState::Response(_) | PipelineState::None | PipelineState::Error => {
None
}
}
}
}
struct PipelineInfo<S> {
req: UnsafeCell<HttpRequest<S>>,
count: u16,
mws: Rc<Vec<Box<Middleware<S>>>>,
context: Option<Box<ActorHttpContext>>,
error: Option<Error>,
disconnected: Option<bool>,
encoding: ContentEncoding,
}
impl<S> PipelineInfo<S> {
fn new(req: HttpRequest<S>) -> PipelineInfo<S> {
PipelineInfo {
req: UnsafeCell::new(req),
count: 0,
mws: Rc::new(Vec::new()),
error: None,
context: None,
disconnected: None,
encoding: ContentEncoding::Auto,
}
}
#[inline]
fn req(&self) -> &HttpRequest<S> {
unsafe { &*self.req.get() }
}
#[inline]
#[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))]
fn req_mut(&self) -> &mut HttpRequest<S> {
#[allow(mutable_transmutes)]
unsafe {
&mut *self.req.get()
}
}
fn poll_context(&mut self) -> Poll<(), Error> {
if let Some(ref mut context) = self.context {
match context.poll() {
Err(err) => Err(err),
Ok(Async::NotReady) => Ok(Async::NotReady),
Ok(Async::Ready(_)) => Ok(Async::Ready(())),
}
} else {
Ok(Async::Ready(()))
}
}
}
impl<S: 'static, H: PipelineHandler<S>> Pipeline<S, H> {
pub fn new(
req: HttpRequest<S>, mws: Rc<Vec<Box<Middleware<S>>>>,
handler: Rc<UnsafeCell<H>>, htype: HandlerType,
) -> Pipeline<S, H> {
let mut info = PipelineInfo {
mws,
req: UnsafeCell::new(req),
count: 0,
error: None,
context: None,
disconnected: None,
encoding: unsafe { &*handler.get() }.encoding(),
};
let state = StartMiddlewares::init(&mut info, handler, htype);
Pipeline(info, state)
}
}
impl Pipeline<(), Inner<()>> {
pub fn error<R: Into<HttpResponse>>(err: R) -> Box<HttpHandlerTask> {
Box::new(Pipeline::<(), Inner<()>>(
PipelineInfo::new(HttpRequest::default()),
ProcessResponse::init(err.into()),
))
}
}
impl<S: 'static, H> Pipeline<S, H> {
#[inline]
fn is_done(&self) -> bool {
match self.1 {
PipelineState::None
| PipelineState::Error
| PipelineState::Starting(_)
| PipelineState::Handler(_)
| PipelineState::RunMiddlewares(_)
| PipelineState::Response(_) => true,
PipelineState::Finishing(_) | PipelineState::Completed(_) => false,
}
}
}
impl<S: 'static, H: PipelineHandler<S>> HttpHandlerTask for Pipeline<S, H> {
fn disconnected(&mut self) {
self.0.disconnected = Some(true);
}
fn poll_io(&mut self, io: &mut Writer) -> Poll<bool, Error> {
let info: &mut PipelineInfo<_> = unsafe { &mut *(&mut self.0 as *mut _) };
loop {
if self.1.is_response() {
let state = mem::replace(&mut self.1, PipelineState::None);
if let PipelineState::Response(st) = state {
match st.poll_io(io, info) {
Ok(state) => {
self.1 = state;
if let Some(error) = self.0.error.take() {
return Err(error);
} else {
return Ok(Async::Ready(self.is_done()));
}
}
Err(state) => {
self.1 = state;
return Ok(Async::NotReady);
}
}
}
}
match self.1 {
PipelineState::None => return Ok(Async::Ready(true)),
PipelineState::Error => {
return Err(
io::Error::new(io::ErrorKind::Other, "Internal error").into()
)
}
_ => (),
}
match self.1.poll(info) {
Some(state) => self.1 = state,
None => return Ok(Async::NotReady),
}
}
}
fn poll(&mut self) -> Poll<(), Error> {
let info: &mut PipelineInfo<_> = unsafe { &mut *(&mut self.0 as *mut _) };
loop {
match self.1 {
PipelineState::None | PipelineState::Error => {
return Ok(Async::Ready(()))
}
_ => (),
}
if let Some(state) = self.1.poll(info) {
self.1 = state;
} else {
return Ok(Async::NotReady);
}
}
}
}
type Fut = Box<Future<Item = Option<HttpResponse>, Error = Error>>;
/// Middlewares start executor
struct StartMiddlewares<S, H> {
hnd: Rc<UnsafeCell<H>>,
htype: HandlerType,
fut: Option<Fut>,
_s: PhantomData<S>,
}
impl<S: 'static, H: PipelineHandler<S>> StartMiddlewares<S, H> {
fn init(
info: &mut PipelineInfo<S>, hnd: Rc<UnsafeCell<H>>, htype: HandlerType,
) -> PipelineState<S, H> {
// execute middlewares, we need this stage because middlewares could be
// non-async and we can move to next state immediately
let len = info.mws.len() as u16;
loop {
if info.count == len {
let reply = unsafe { &mut *hnd.get() }.handle(info.req().clone(), htype);
return WaitingResponse::init(info, reply);
} else {
match info.mws[info.count as usize].start(info.req_mut()) {
Ok(Started::Done) => info.count += 1,
Ok(Started::Response(resp)) => {
return RunMiddlewares::init(info, resp)
}
Ok(Started::Future(fut)) => {
return PipelineState::Starting(StartMiddlewares {
hnd,
htype,
fut: Some(fut),
_s: PhantomData,
})
}
Err(err) => return RunMiddlewares::init(info, err.into()),
}
}
}
}
fn poll(&mut self, info: &mut PipelineInfo<S>) -> Option<PipelineState<S, H>> {
let len = info.mws.len() as u16;
'outer: loop {
match self.fut.as_mut().unwrap().poll() {
Ok(Async::NotReady) => return None,
Ok(Async::Ready(resp)) => {
info.count += 1;
if let Some(resp) = resp {
return Some(RunMiddlewares::init(info, resp));
}
loop {
if info.count == len {
let reply = unsafe { &mut *self.hnd.get() }
.handle(info.req().clone(), self.htype);
return Some(WaitingResponse::init(info, reply));
} else {
match info.mws[info.count as usize].start(info.req_mut()) {
Ok(Started::Done) => info.count += 1,
Ok(Started::Response(resp)) => {
return Some(RunMiddlewares::init(info, resp));
}
Ok(Started::Future(fut)) => {
self.fut = Some(fut);
continue 'outer;
}
Err(err) => {
return Some(RunMiddlewares::init(info, err.into()))
}
}
}
}
}
Err(err) => return Some(RunMiddlewares::init(info, err.into())),
}
}
}
}
// waiting for response
struct WaitingResponse<S, H> {
fut: Box<Future<Item = HttpResponse, Error = Error>>,
_s: PhantomData<S>,
_h: PhantomData<H>,
}
impl<S: 'static, H> WaitingResponse<S, H> {
#[inline]
fn init(
info: &mut PipelineInfo<S>, reply: AsyncResult<HttpResponse>,
) -> PipelineState<S, H> {
match reply.into() {
AsyncResultItem::Err(err) => RunMiddlewares::init(info, err.into()),
AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, resp),
AsyncResultItem::Future(fut) => PipelineState::Handler(WaitingResponse {
fut,
_s: PhantomData,
_h: PhantomData,
}),
}
}
fn poll(&mut self, info: &mut PipelineInfo<S>) -> Option<PipelineState<S, H>> {
match self.fut.poll() {
Ok(Async::NotReady) => None,
Ok(Async::Ready(response)) => Some(RunMiddlewares::init(info, response)),
Err(err) => Some(RunMiddlewares::init(info, err.into())),
}
}
}
/// Middlewares response executor
struct RunMiddlewares<S, H> {
curr: usize,
fut: Option<Box<Future<Item = HttpResponse, Error = Error>>>,
_s: PhantomData<S>,
_h: PhantomData<H>,
}
impl<S: 'static, H> RunMiddlewares<S, H> {
#[inline]
fn init(info: &mut PipelineInfo<S>, mut resp: HttpResponse) -> PipelineState<S, H> {
if info.count == 0 {
return ProcessResponse::init(resp);
}
let mut curr = 0;
let len = info.mws.len();
loop {
resp = match info.mws[curr].response(info.req_mut(), resp) {
Err(err) => {
info.count = (curr + 1) as u16;
return ProcessResponse::init(err.into());
}
Ok(Response::Done(r)) => {
curr += 1;
if curr == len {
return ProcessResponse::init(r);
} else {
r
}
}
Ok(Response::Future(fut)) => {
return PipelineState::RunMiddlewares(RunMiddlewares {
curr,
fut: Some(fut),
_s: PhantomData,
_h: PhantomData,
})
}
};
}
}
fn poll(&mut self, info: &mut PipelineInfo<S>) -> Option<PipelineState<S, H>> {
let len = info.mws.len();
loop {
// poll latest fut
let mut resp = match self.fut.as_mut().unwrap().poll() {
Ok(Async::NotReady) => return None,
Ok(Async::Ready(resp)) => {
self.curr += 1;
resp
}
Err(err) => return Some(ProcessResponse::init(err.into())),
};
loop {
if self.curr == len {
return Some(ProcessResponse::init(resp));
} else {
match info.mws[self.curr].response(info.req_mut(), resp) {
Err(err) => return Some(ProcessResponse::init(err.into())),
Ok(Response::Done(r)) => {
self.curr += 1;
resp = r
}
Ok(Response::Future(fut)) => {
self.fut = Some(fut);
break;
}
}
}
}
}
}
}
struct ProcessResponse<S, H> {
resp: HttpResponse,
iostate: IOState,
running: RunningState,
drain: Option<oneshot::Sender<()>>,
_s: PhantomData<S>,
_h: PhantomData<H>,
}
#[derive(PartialEq)]
enum RunningState {
Running,
Paused,
Done,
}
impl RunningState {
#[inline]
fn pause(&mut self) {
if *self != RunningState::Done {
*self = RunningState::Paused
}
}
#[inline]
fn resume(&mut self) {
if *self != RunningState::Done {
*self = RunningState::Running
}
}
}
enum IOState {
Response,
Payload(BodyStream),
Actor(Box<ActorHttpContext>),
Done,
}
impl<S: 'static, H> ProcessResponse<S, H> {
#[inline]
fn init(resp: HttpResponse) -> PipelineState<S, H> {
PipelineState::Response(ProcessResponse {
resp,
iostate: IOState::Response,
running: RunningState::Running,
drain: None,
_s: PhantomData,
_h: PhantomData,
})
}
fn poll_io(
mut self, io: &mut Writer, info: &mut PipelineInfo<S>,
) -> Result<PipelineState<S, H>, PipelineState<S, H>> {
loop {
if self.drain.is_none() && self.running != RunningState::Paused {
// if task is paused, write buffer is probably full
'inner: loop {
let result = match mem::replace(&mut self.iostate, IOState::Done) {
IOState::Response => {
let encoding =
self.resp.content_encoding().unwrap_or(info.encoding);
let result = match io.start(
info.req_mut().get_inner(),
&mut self.resp,
encoding,
) {
Ok(res) => res,
Err(err) => {
info.error = Some(err.into());
return Ok(FinishingMiddlewares::init(
info, self.resp,
));
}
};
if let Some(err) = self.resp.error() {
if self.resp.status().is_server_error() {
error!(
"Error occured during request handling, status: {} {}",
self.resp.status(), err
);
} else {
warn!(
"Error occured during request handling: {}",
err
);
}
if log_enabled!(Debug) {
debug!("{:?}", err);
}
}
// always poll stream or actor for the first time
match self.resp.replace_body(Body::Empty) {
Body::Streaming(stream) => {
self.iostate = IOState::Payload(stream);
continue 'inner;
}
Body::Actor(ctx) => {
self.iostate = IOState::Actor(ctx);
continue 'inner;
}
_ => (),
}
result
}
IOState::Payload(mut body) => match body.poll() {
Ok(Async::Ready(None)) => {
if let Err(err) = io.write_eof() {
info.error = Some(err.into());
return Ok(FinishingMiddlewares::init(
info, self.resp,
));
}
break;
}
Ok(Async::Ready(Some(chunk))) => {
self.iostate = IOState::Payload(body);
match io.write(chunk.into()) {
Err(err) => {
info.error = Some(err.into());
return Ok(FinishingMiddlewares::init(
info, self.resp,
));
}
Ok(result) => result,
}
}
Ok(Async::NotReady) => {
self.iostate = IOState::Payload(body);
break;
}
Err(err) => {
info.error = Some(err);
return Ok(FinishingMiddlewares::init(info, self.resp));
}
},
IOState::Actor(mut ctx) => {
if info.disconnected.take().is_some() {
ctx.disconnected();
}
match ctx.poll() {
Ok(Async::Ready(Some(vec))) => {
if vec.is_empty() {
self.iostate = IOState::Actor(ctx);
break;
}
let mut res = None;
for frame in vec {
match frame {
Frame::Chunk(None) => {
info.context = Some(ctx);
if let Err(err) = io.write_eof() {
info.error = Some(err.into());
return Ok(
FinishingMiddlewares::init(
info, self.resp,
),
);
}
break 'inner;
}
Frame::Chunk(Some(chunk)) => {
match io.write(chunk) {
Err(err) => {
info.error = Some(err.into());
return Ok(
FinishingMiddlewares::init(
info, self.resp,
),
);
}
Ok(result) => res = Some(result),
}
}
Frame::Drain(fut) => self.drain = Some(fut),
}
}
self.iostate = IOState::Actor(ctx);
if self.drain.is_some() {
self.running.resume();
break 'inner;
}
res.unwrap()
}
Ok(Async::Ready(None)) => break,
Ok(Async::NotReady) => {
self.iostate = IOState::Actor(ctx);
break;
}
Err(err) => {
info.error = Some(err);
return Ok(FinishingMiddlewares::init(
info, self.resp,
));
}
}
}
IOState::Done => break,
};
match result {
WriterState::Pause => {
self.running.pause();
break;
}
WriterState::Done => self.running.resume(),
}
}
}
// flush io but only if we need to
if self.running == RunningState::Paused || self.drain.is_some() {
match io.poll_completed(false) {
Ok(Async::Ready(_)) => {
self.running.resume();
// resolve drain futures
if let Some(tx) = self.drain.take() {
let _ = tx.send(());
}
// restart io processing
continue;
}
Ok(Async::NotReady) => return Err(PipelineState::Response(self)),
Err(err) => {
info.error = Some(err.into());
return Ok(FinishingMiddlewares::init(info, self.resp));
}
}
}
break;
}
// response is completed
match self.iostate {
IOState::Done => {
match io.write_eof() {
Ok(_) => (),
Err(err) => {
info.error = Some(err.into());
return Ok(FinishingMiddlewares::init(info, self.resp));
}
}
self.resp.set_response_size(io.written());
Ok(FinishingMiddlewares::init(info, self.resp))
}
_ => Err(PipelineState::Response(self)),
}
}
}
/// Middlewares start executor
struct FinishingMiddlewares<S, H> {
resp: HttpResponse,
fut: Option<Box<Future<Item = (), Error = Error>>>,
_s: PhantomData<S>,
_h: PhantomData<H>,
}
impl<S: 'static, H> FinishingMiddlewares<S, H> {
#[inline]
fn init(info: &mut PipelineInfo<S>, resp: HttpResponse) -> PipelineState<S, H> {
if info.count == 0 {
Completed::init(info)
} else {
let mut state = FinishingMiddlewares {
resp,
fut: None,
_s: PhantomData,
_h: PhantomData,
};
if let Some(st) = state.poll(info) {
st
} else {
PipelineState::Finishing(state)
}
}
}
fn poll(&mut self, info: &mut PipelineInfo<S>) -> Option<PipelineState<S, H>> {
loop {
// poll latest fut
let not_ready = if let Some(ref mut fut) = self.fut {
match fut.poll() {
Ok(Async::NotReady) => true,
Ok(Async::Ready(())) => false,
Err(err) => {
error!("Middleware finish error: {}", err);
false
}
}
} else {
false
};
if not_ready {
return None;
}
self.fut = None;
if info.count == 0 {
return Some(Completed::init(info));
}
info.count -= 1;
match info.mws[info.count as usize].finish(info.req_mut(), &self.resp) {
Finished::Done => {
if info.count == 0 {
return Some(Completed::init(info));
}
}
Finished::Future(fut) => {
self.fut = Some(fut);
}
}
}
}
}
#[derive(Debug)]
struct Completed<S, H>(PhantomData<S>, PhantomData<H>);
impl<S, H> Completed<S, H> {
#[inline]
fn init(info: &mut PipelineInfo<S>) -> PipelineState<S, H> {
if let Some(ref err) = info.error {
error!("Error occurred during request handling: {}", err);
}
if info.context.is_none() {
PipelineState::None
} else {
PipelineState::Completed(Completed(PhantomData, PhantomData))
}
}
#[inline]
fn poll(&mut self, info: &mut PipelineInfo<S>) -> Option<PipelineState<S, H>> {
match info.poll_context() {
Ok(Async::NotReady) => None,
Ok(Async::Ready(())) => Some(PipelineState::None),
Err(_) => Some(PipelineState::Error),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix::*;
use context::HttpContext;
use futures::future::{lazy, result};
use tokio_core::reactor::Core;
impl<S, H> PipelineState<S, H> {
fn is_none(&self) -> Option<bool> {
if let PipelineState::None = *self {
Some(true)
} else {
None
}
}
fn completed(self) -> Option<Completed<S, H>> {
if let PipelineState::Completed(c) = self {
Some(c)
} else {
None
}
}
}
struct MyActor;
impl Actor for MyActor {
type Context = HttpContext<MyActor>;
}
#[test]
fn test_completed() {
Core::new()
.unwrap()
.run(lazy(|| {
let mut info = PipelineInfo::new(HttpRequest::default());
Completed::<(), Inner<()>>::init(&mut info)
.is_none()
.unwrap();
let req = HttpRequest::default();
let mut ctx = HttpContext::new(req.clone(), MyActor);
let addr: Addr<Unsync, _> = ctx.address();
let mut info = PipelineInfo::new(req);
info.context = Some(Box::new(ctx));
let mut state = Completed::<(), Inner<()>>::init(&mut info)
.completed()
.unwrap();
assert!(state.poll(&mut info).is_none());
let pp = Pipeline(info, PipelineState::Completed(state));
assert!(!pp.is_done());
let Pipeline(mut info, st) = pp;
let mut st = st.completed().unwrap();
drop(addr);
assert!(st.poll(&mut info).unwrap().is_none().unwrap());
result(Ok::<_, ()>(()))
}))
.unwrap();
}
}

400
src/pred.rs Normal file
View File

@ -0,0 +1,400 @@
//! Route match predicates
#![allow(non_snake_case)]
use http;
use http::{header, HttpTryFrom};
use httpmessage::HttpMessage;
use httprequest::HttpRequest;
use std::marker::PhantomData;
/// Trait defines resource route predicate.
/// Predicate can modify request object. It is also possible to
/// to store extra attributes on request by using `Extensions` container,
/// Extensions container available via `HttpRequest::extensions()` method.
pub trait Predicate<S> {
/// Check if request matches predicate
fn check(&self, &mut HttpRequest<S>) -> bool;
}
/// Return predicate that matches if any of supplied predicate matches.
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::{pred, App, HttpResponse};
///
/// fn main() {
/// App::new()
/// .resource("/index.html", |r| r.route()
/// .filter(pred::Any(pred::Get()).or(pred::Post()))
/// .f(|r| HttpResponse::MethodNotAllowed()));
/// }
/// ```
pub fn Any<S: 'static, P: Predicate<S> + 'static>(pred: P) -> AnyPredicate<S> {
AnyPredicate(vec![Box::new(pred)])
}
/// Matches if any of supplied predicate matches.
pub struct AnyPredicate<S>(Vec<Box<Predicate<S>>>);
impl<S> AnyPredicate<S> {
/// Add new predicate to list of predicates to check
pub fn or<P: Predicate<S> + 'static>(mut self, pred: P) -> Self {
self.0.push(Box::new(pred));
self
}
}
impl<S: 'static> Predicate<S> for AnyPredicate<S> {
fn check(&self, req: &mut HttpRequest<S>) -> bool {
for p in &self.0 {
if p.check(req) {
return true;
}
}
false
}
}
/// Return predicate that matches if all of supplied predicate matches.
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::{pred, App, HttpResponse};
///
/// fn main() {
/// App::new()
/// .resource("/index.html", |r| r.route()
/// .filter(pred::All(pred::Get())
/// .and(pred::Header("content-type", "plain/text")))
/// .f(|_| HttpResponse::MethodNotAllowed()));
/// }
/// ```
pub fn All<S: 'static, P: Predicate<S> + 'static>(pred: P) -> AllPredicate<S> {
AllPredicate(vec![Box::new(pred)])
}
/// Matches if all of supplied predicate matches.
pub struct AllPredicate<S>(Vec<Box<Predicate<S>>>);
impl<S> AllPredicate<S> {
/// Add new predicate to list of predicates to check
pub fn and<P: Predicate<S> + 'static>(mut self, pred: P) -> Self {
self.0.push(Box::new(pred));
self
}
}
impl<S: 'static> Predicate<S> for AllPredicate<S> {
fn check(&self, req: &mut HttpRequest<S>) -> bool {
for p in &self.0 {
if !p.check(req) {
return false;
}
}
true
}
}
/// Return predicate that matches if supplied predicate does not match.
pub fn Not<S: 'static, P: Predicate<S> + 'static>(pred: P) -> NotPredicate<S> {
NotPredicate(Box::new(pred))
}
#[doc(hidden)]
pub struct NotPredicate<S>(Box<Predicate<S>>);
impl<S: 'static> Predicate<S> for NotPredicate<S> {
fn check(&self, req: &mut HttpRequest<S>) -> bool {
!self.0.check(req)
}
}
/// Http method predicate
#[doc(hidden)]
pub struct MethodPredicate<S>(http::Method, PhantomData<S>);
impl<S: 'static> Predicate<S> for MethodPredicate<S> {
fn check(&self, req: &mut HttpRequest<S>) -> bool {
*req.method() == self.0
}
}
/// Predicate to match *GET* http method
pub fn Get<S: 'static>() -> MethodPredicate<S> {
MethodPredicate(http::Method::GET, PhantomData)
}
/// Predicate to match *POST* http method
pub fn Post<S: 'static>() -> MethodPredicate<S> {
MethodPredicate(http::Method::POST, PhantomData)
}
/// Predicate to match *PUT* http method
pub fn Put<S: 'static>() -> MethodPredicate<S> {
MethodPredicate(http::Method::PUT, PhantomData)
}
/// Predicate to match *DELETE* http method
pub fn Delete<S: 'static>() -> MethodPredicate<S> {
MethodPredicate(http::Method::DELETE, PhantomData)
}
/// Predicate to match *HEAD* http method
pub fn Head<S: 'static>() -> MethodPredicate<S> {
MethodPredicate(http::Method::HEAD, PhantomData)
}
/// Predicate to match *OPTIONS* http method
pub fn Options<S: 'static>() -> MethodPredicate<S> {
MethodPredicate(http::Method::OPTIONS, PhantomData)
}
/// Predicate to match *CONNECT* http method
pub fn Connect<S: 'static>() -> MethodPredicate<S> {
MethodPredicate(http::Method::CONNECT, PhantomData)
}
/// Predicate to match *PATCH* http method
pub fn Patch<S: 'static>() -> MethodPredicate<S> {
MethodPredicate(http::Method::PATCH, PhantomData)
}
/// Predicate to match *TRACE* http method
pub fn Trace<S: 'static>() -> MethodPredicate<S> {
MethodPredicate(http::Method::TRACE, PhantomData)
}
/// Predicate to match specified http method
pub fn Method<S: 'static>(method: http::Method) -> MethodPredicate<S> {
MethodPredicate(method, PhantomData)
}
/// Return predicate that matches if request contains specified header and
/// value.
pub fn Header<S: 'static>(
name: &'static str, value: &'static str,
) -> HeaderPredicate<S> {
HeaderPredicate(
header::HeaderName::try_from(name).unwrap(),
header::HeaderValue::from_static(value),
PhantomData,
)
}
#[doc(hidden)]
pub struct HeaderPredicate<S>(header::HeaderName, header::HeaderValue, PhantomData<S>);
impl<S: 'static> Predicate<S> for HeaderPredicate<S> {
fn check(&self, req: &mut HttpRequest<S>) -> bool {
if let Some(val) = req.headers().get(&self.0) {
return val == self.1;
}
false
}
}
/// Return predicate that matches if request contains specified Host name.
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::{pred, App, HttpResponse};
///
/// fn main() {
/// App::new().resource("/index.html", |r| {
/// r.route()
/// .filter(pred::Host("www.rust-lang.org"))
/// .f(|_| HttpResponse::MethodNotAllowed())
/// });
/// }
/// ```
pub fn Host<S: 'static, H: AsRef<str>>(host: H) -> HostPredicate<S> {
HostPredicate(host.as_ref().to_string(), None, PhantomData)
}
#[doc(hidden)]
pub struct HostPredicate<S>(String, Option<String>, PhantomData<S>);
impl<S> HostPredicate<S> {
/// Set reuest scheme to match
pub fn scheme<H: AsRef<str>>(&mut self, scheme: H) {
self.1 = Some(scheme.as_ref().to_string())
}
}
impl<S: 'static> Predicate<S> for HostPredicate<S> {
fn check(&self, req: &mut HttpRequest<S>) -> bool {
let info = req.connection_info();
if let Some(ref scheme) = self.1 {
self.0 == info.host() && scheme == info.scheme()
} else {
self.0 == info.host()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::header::{self, HeaderMap};
use http::{Method, Uri, Version};
use std::str::FromStr;
#[test]
fn test_header() {
let mut headers = HeaderMap::new();
headers.insert(
header::TRANSFER_ENCODING,
header::HeaderValue::from_static("chunked"),
);
let mut req = HttpRequest::new(
Method::GET,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
headers,
None,
);
let pred = Header("transfer-encoding", "chunked");
assert!(pred.check(&mut req));
let pred = Header("transfer-encoding", "other");
assert!(!pred.check(&mut req));
let pred = Header("content-type", "other");
assert!(!pred.check(&mut req));
}
#[test]
fn test_host() {
let mut headers = HeaderMap::new();
headers.insert(
header::HOST,
header::HeaderValue::from_static("www.rust-lang.org"),
);
let mut req = HttpRequest::new(
Method::GET,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
headers,
None,
);
let pred = Host("www.rust-lang.org");
assert!(pred.check(&mut req));
let pred = Host("localhost");
assert!(!pred.check(&mut req));
}
#[test]
fn test_methods() {
let mut req = HttpRequest::new(
Method::GET,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
let mut req2 = HttpRequest::new(
Method::POST,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
assert!(Get().check(&mut req));
assert!(!Get().check(&mut req2));
assert!(Post().check(&mut req2));
assert!(!Post().check(&mut req));
let mut r = HttpRequest::new(
Method::PUT,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
assert!(Put().check(&mut r));
assert!(!Put().check(&mut req));
let mut r = HttpRequest::new(
Method::DELETE,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
assert!(Delete().check(&mut r));
assert!(!Delete().check(&mut req));
let mut r = HttpRequest::new(
Method::HEAD,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
assert!(Head().check(&mut r));
assert!(!Head().check(&mut req));
let mut r = HttpRequest::new(
Method::OPTIONS,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
assert!(Options().check(&mut r));
assert!(!Options().check(&mut req));
let mut r = HttpRequest::new(
Method::CONNECT,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
assert!(Connect().check(&mut r));
assert!(!Connect().check(&mut req));
let mut r = HttpRequest::new(
Method::PATCH,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
assert!(Patch().check(&mut r));
assert!(!Patch().check(&mut req));
let mut r = HttpRequest::new(
Method::TRACE,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
assert!(Trace().check(&mut r));
assert!(!Trace().check(&mut req));
}
#[test]
fn test_preds() {
let mut r = HttpRequest::new(
Method::TRACE,
Uri::from_str("/").unwrap(),
Version::HTTP_11,
HeaderMap::new(),
None,
);
assert!(Not(Get()).check(&mut r));
assert!(!Not(Trace()).check(&mut r));
assert!(All(Trace()).and(Trace()).check(&mut r));
assert!(!All(Get()).and(Trace()).check(&mut r));
assert!(Any(Get()).or(Trace()).check(&mut r));
assert!(!Any(Get()).or(Get()).check(&mut r));
}
}

View File

@ -1,916 +0,0 @@
use std::{self, io, ptr};
use httparse;
use http::{Method, Version, HttpTryFrom, HeaderMap};
use http::header::{self, HeaderName, HeaderValue};
use bytes::{BytesMut, BufMut};
use futures::{Async, Poll};
use tokio_io::AsyncRead;
use percent_encoding;
use error::ParseError;
use decode::Decoder;
use httprequest::HttpRequest;
use payload::{Payload, PayloadError, PayloadSender};
const MAX_HEADERS: usize = 100;
const INIT_BUFFER_SIZE: usize = 8192;
const MAX_BUFFER_SIZE: usize = 131_072;
enum Decoding {
Paused,
Ready,
NotReady,
}
struct PayloadInfo {
tx: PayloadSender,
decoder: Decoder,
}
pub(crate) struct Reader {
read_buf: BytesMut,
payload: Option<PayloadInfo>,
}
#[derive(Debug)]
pub(crate) enum ReaderError {
Payload,
Error(ParseError),
}
impl Reader {
pub fn new() -> Reader {
Reader {
read_buf: BytesMut::new(),
payload: None,
}
}
fn decode(&mut self) -> std::result::Result<Decoding, ReaderError>
{
if let Some(ref mut payload) = self.payload {
if payload.tx.maybe_paused() {
return Ok(Decoding::Paused)
}
loop {
match payload.decoder.decode(&mut self.read_buf) {
Ok(Async::Ready(Some(bytes))) => {
payload.tx.feed_data(bytes)
},
Ok(Async::Ready(None)) => {
payload.tx.feed_eof();
return Ok(Decoding::Ready)
},
Ok(Async::NotReady) => return Ok(Decoding::NotReady),
Err(err) => {
payload.tx.set_error(err.into());
return Err(ReaderError::Payload)
}
}
}
} else {
return Ok(Decoding::Ready)
}
}
pub fn parse<T>(&mut self, io: &mut T) -> Poll<(HttpRequest, Payload), ReaderError>
where T: AsyncRead
{
loop {
match self.decode()? {
Decoding::Paused => return Ok(Async::NotReady),
Decoding::Ready => {
self.payload = None;
break
},
Decoding::NotReady => {
match self.read_from_io(io) {
Ok(Async::Ready(0)) => {
if let Some(ref mut payload) = self.payload {
payload.tx.set_error(PayloadError::Incomplete);
}
// http channel should not deal with payload errors
return Err(ReaderError::Payload)
}
Ok(Async::Ready(_)) => {
continue
}
Ok(Async::NotReady) => break,
Err(err) => {
if let Some(ref mut payload) = self.payload {
payload.tx.set_error(err.into());
}
// http channel should not deal with payload errors
return Err(ReaderError::Payload)
}
}
}
}
}
loop {
match try!(Reader::parse_message(&mut self.read_buf).map_err(ReaderError::Error)) {
Some((msg, decoder)) => {
let payload = if let Some(decoder) = decoder {
let (tx, rx) = Payload::new(false);
let payload = PayloadInfo {
tx: tx,
decoder: decoder,
};
self.payload = Some(payload);
loop {
match self.decode()? {
Decoding::Paused =>
break,
Decoding::Ready => {
self.payload = None;
break
},
Decoding::NotReady => {
match self.read_from_io(io) {
Ok(Async::Ready(0)) => {
trace!("parse eof");
if let Some(ref mut payload) = self.payload {
payload.tx.set_error(PayloadError::Incomplete);
}
// http channel should deal with payload errors
return Err(ReaderError::Payload)
}
Ok(Async::Ready(_)) => {
continue
}
Ok(Async::NotReady) => break,
Err(err) => {
if let Some(ref mut payload) = self.payload {
payload.tx.set_error(err.into());
}
// http channel should deal with payload errors
return Err(ReaderError::Payload)
}
}
}
}
}
rx
} else {
let (_, rx) = Payload::new(true);
rx
};
return Ok(Async::Ready((msg, payload)));
},
None => {
if self.read_buf.capacity() >= MAX_BUFFER_SIZE {
debug!("MAX_BUFFER_SIZE reached, closing");
return Err(ReaderError::Error(ParseError::TooLarge));
}
},
}
match self.read_from_io(io) {
Ok(Async::Ready(0)) => {
trace!("Eof during parse");
return Err(ReaderError::Error(ParseError::Incomplete));
},
Ok(Async::Ready(_)) => (),
Ok(Async::NotReady) =>
return Ok(Async::NotReady),
Err(err) =>
return Err(ReaderError::Error(err.into()))
}
}
}
fn read_from_io<T: AsyncRead>(&mut self, io: &mut T) -> Poll<usize, io::Error> {
if self.read_buf.remaining_mut() < INIT_BUFFER_SIZE {
self.read_buf.reserve(INIT_BUFFER_SIZE);
unsafe { // Zero out unused memory
let buf = self.read_buf.bytes_mut();
let len = buf.len();
ptr::write_bytes(buf.as_mut_ptr(), 0, len);
}
}
unsafe {
let n = match io.read(self.read_buf.bytes_mut()) {
Ok(n) => n,
Err(e) => {
if e.kind() == io::ErrorKind::WouldBlock {
return Ok(Async::NotReady);
}
return Err(e)
}
};
self.read_buf.advance_mut(n);
Ok(Async::Ready(n))
}
}
fn parse_message(buf: &mut BytesMut)
-> Result<Option<(HttpRequest, Option<Decoder>)>, ParseError>
{
if buf.is_empty() {
return Ok(None);
}
// Parse http message
let mut headers_indices = [HeaderIndices {
name: (0, 0),
value: (0, 0)
}; MAX_HEADERS];
let (len, method, path, version, headers_len) = {
let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS];
trace!("Request.parse([Header; {}], [u8; {}])", headers.len(), buf.len());
let mut req = httparse::Request::new(&mut headers);
match try!(req.parse(buf)) {
httparse::Status::Complete(len) => {
trace!("Request.parse Complete({})", len);
let method = Method::try_from(req.method.unwrap())
.map_err(|_| ParseError::Method)?;
let path = req.path.unwrap();
let bytes_ptr = buf.as_ref().as_ptr() as usize;
let path_start = path.as_ptr() as usize - bytes_ptr;
let path_end = path_start + path.len();
let path = (path_start, path_end);
let version = if req.version.unwrap() == 1 {
Version::HTTP_11
} else {
Version::HTTP_10
};
record_header_indices(buf.as_ref(), req.headers, &mut headers_indices);
let headers_len = req.headers.len();
(len, method, path, version, headers_len)
}
httparse::Status::Partial => return Ok(None),
}
};
let slice = buf.split_to(len).freeze();
let path = slice.slice(path.0, path.1);
// manually split path, path was found to be utf8 by httparse
let uri = {
if let Ok(path) = percent_encoding::percent_decode(&path).decode_utf8() {
let parts: Vec<&str> = path.splitn(2, '?').collect();
if parts.len() == 2 {
Some((parts[0].to_owned(), parts[1].to_owned()))
} else {
Some((parts[0].to_owned(), String::new()))
}
} else {
None
}
};
let (path, query) = if let Some(uri) = uri {
uri
} else {
let parts: Vec<&str> = unsafe{
std::str::from_utf8_unchecked(&path)}.splitn(2, '?').collect();
if parts.len() == 2 {
(parts[0].to_owned(), parts[1][1..].to_owned())
} else {
(parts[0].to_owned(), String::new())
}
};
// convert headers
let mut headers = HeaderMap::with_capacity(headers_len);
for header in headers_indices[..headers_len].iter() {
if let Ok(name) = HeaderName::try_from(slice.slice(header.name.0, header.name.1)) {
if let Ok(value) = HeaderValue::try_from(
slice.slice(header.value.0, header.value.1))
{
headers.append(name, value);
} else {
return Err(ParseError::Header)
}
} else {
return Err(ParseError::Header)
}
}
let msg = HttpRequest::new(method, path, version, headers, query);
let decoder = if msg.upgrade() {
Some(Decoder::eof())
} else {
let chunked = msg.chunked()?;
// Content-Length
if let Some(len) = msg.headers().get(header::CONTENT_LENGTH) {
if chunked {
return Err(ParseError::Header)
}
if let Ok(s) = len.to_str() {
if let Ok(len) = s.parse::<u64>() {
Some(Decoder::length(len))
} else {
debug!("illegal Content-Length: {:?}", len);
return Err(ParseError::Header)
}
} else {
debug!("illegal Content-Length: {:?}", len);
return Err(ParseError::Header)
}
} else if chunked {
Some(Decoder::chunked())
} else {
None
}
};
Ok(Some((msg, decoder)))
}
}
#[derive(Clone, Copy)]
struct HeaderIndices {
name: (usize, usize),
value: (usize, usize),
}
fn record_header_indices(bytes: &[u8],
headers: &[httparse::Header],
indices: &mut [HeaderIndices])
{
let bytes_ptr = bytes.as_ptr() as usize;
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
let name_start = header.name.as_ptr() as usize - bytes_ptr;
let name_end = name_start + header.name.len();
indices.name = (name_start, name_end);
let value_start = header.value.as_ptr() as usize - bytes_ptr;
let value_end = value_start + header.value.len();
indices.value = (value_start, value_end);
}
}
#[cfg(test)]
mod tests {
use std::{io, cmp};
use bytes::{Bytes, BytesMut};
use futures::{Async};
use tokio_io::AsyncRead;
use http::{Version, Method};
use super::{Reader, ReaderError};
struct Buffer {
buf: Bytes,
err: Option<io::Error>,
}
impl Buffer {
fn new(data: &'static str) -> Buffer {
Buffer {
buf: Bytes::from(data),
err: None,
}
}
fn feed_data(&mut self, data: &'static str) {
let mut b = BytesMut::from(self.buf.as_ref());
b.extend(data.as_bytes());
self.buf = b.take().freeze();
}
}
impl AsyncRead for Buffer {}
impl io::Read for Buffer {
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
if self.buf.is_empty() {
if self.err.is_some() {
Err(self.err.take().unwrap())
} else {
Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
}
} else {
let size = cmp::min(self.buf.len(), dst.len());
let b = self.buf.split_to(size);
dst[..size].copy_from_slice(&b);
Ok(size)
}
}
}
macro_rules! not_ready {
($e:expr) => (match $e {
Ok(Async::NotReady) => (),
Err(err) => panic!("Unexpected error: {:?}", err),
_ => panic!("Should not be ready"),
})
}
macro_rules! parse_ready {
($e:expr) => (
match Reader::new().parse($e) {
Ok(Async::Ready((req, payload))) => (req, payload),
Ok(_) => panic!("Eof during parsing http request"),
Err(err) => panic!("Error during parsing http request: {:?}", err),
}
)
}
macro_rules! reader_parse_ready {
($e:expr) => (
match $e {
Ok(Async::Ready((req, payload))) => (req, payload),
Ok(_) => panic!("Eof during parsing http request"),
Err(err) => panic!("Error during parsing http request: {:?}", err),
}
)
}
macro_rules! expect_parse_err {
($e:expr) => (match Reader::new().parse($e) {
Err(err) => match err {
ReaderError::Error(_) => (),
_ => panic!("Parse error expected"),
},
_ => panic!("Error expected"),
})
}
#[test]
fn test_parse() {
let mut buf = Buffer::new("GET /test HTTP/1.1\r\n\r\n");
let mut reader = Reader::new();
match reader.parse(&mut buf) {
Ok(Async::Ready((req, payload))) => {
assert_eq!(req.version(), Version::HTTP_11);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test");
assert!(payload.eof());
}
Ok(_) | Err(_) => panic!("Error during parsing http request"),
}
}
#[test]
fn test_parse_partial() {
let mut buf = Buffer::new("PUT /test HTTP/1");
let mut reader = Reader::new();
match reader.parse(&mut buf) {
Ok(Async::NotReady) => (),
_ => panic!("Error"),
}
buf.feed_data(".1\r\n\r\n");
match reader.parse(&mut buf) {
Ok(Async::Ready((req, payload))) => {
assert_eq!(req.version(), Version::HTTP_11);
assert_eq!(*req.method(), Method::PUT);
assert_eq!(req.path(), "/test");
assert!(payload.eof());
}
Ok(_) | Err(_) => panic!("Error during parsing http request"),
}
}
#[test]
fn test_parse_post() {
let mut buf = Buffer::new("POST /test2 HTTP/1.0\r\n\r\n");
let mut reader = Reader::new();
match reader.parse(&mut buf) {
Ok(Async::Ready((req, payload))) => {
assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::POST);
assert_eq!(req.path(), "/test2");
assert!(payload.eof());
}
Ok(_) | Err(_) => panic!("Error during parsing http request"),
}
}
#[test]
fn test_parse_body() {
let mut buf = Buffer::new("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody");
let mut reader = Reader::new();
match reader.parse(&mut buf) {
Ok(Async::Ready((req, mut payload))) => {
assert_eq!(req.version(), Version::HTTP_11);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test");
assert_eq!(payload.readall().unwrap().as_ref(), b"body");
}
Ok(_) | Err(_) => panic!("Error during parsing http request"),
}
}
#[test]
fn test_parse_body_crlf() {
let mut buf = Buffer::new(
"\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody");
let mut reader = Reader::new();
match reader.parse(&mut buf) {
Ok(Async::Ready((req, mut payload))) => {
assert_eq!(req.version(), Version::HTTP_11);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test");
assert_eq!(payload.readall().unwrap().as_ref(), b"body");
}
Ok(_) | Err(_) => panic!("Error during parsing http request"),
}
}
#[test]
fn test_parse_partial_eof() {
let mut buf = Buffer::new("GET /test HTTP/1.1\r\n");
let mut reader = Reader::new();
not_ready!{ reader.parse(&mut buf) }
buf.feed_data("\r\n");
match reader.parse(&mut buf) {
Ok(Async::Ready((req, payload))) => {
assert_eq!(req.version(), Version::HTTP_11);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test");
assert!(payload.eof());
}
Ok(_) | Err(_) => panic!("Error during parsing http request"),
}
}
#[test]
fn test_headers_split_field() {
let mut buf = Buffer::new("GET /test HTTP/1.1\r\n");
let mut reader = Reader::new();
not_ready!{ reader.parse(&mut buf) }
buf.feed_data("t");
not_ready!{ reader.parse(&mut buf) }
buf.feed_data("es");
not_ready!{ reader.parse(&mut buf) }
buf.feed_data("t: value\r\n\r\n");
match reader.parse(&mut buf) {
Ok(Async::Ready((req, payload))) => {
assert_eq!(req.version(), Version::HTTP_11);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test");
assert_eq!(req.headers().get("test").unwrap().as_bytes(), b"value");
assert!(payload.eof());
}
Ok(_) | Err(_) => panic!("Error during parsing http request"),
}
}
#[test]
fn test_headers_multi_value() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
Set-Cookie: c1=cookie1\r\n\
Set-Cookie: c2=cookie2\r\n\r\n");
let mut reader = Reader::new();
match reader.parse(&mut buf) {
Ok(Async::Ready((req, _))) => {
let val: Vec<_> = req.headers().get_all("Set-Cookie")
.iter().map(|v| v.to_str().unwrap().to_owned()).collect();
assert_eq!(val[0], "c1=cookie1");
assert_eq!(val[1], "c2=cookie2");
}
Ok(_) | Err(_) => panic!("Error during parsing http request"),
}
}
#[test]
fn test_conn_default_1_0() {
let mut buf = Buffer::new("GET /test HTTP/1.0\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert!(!req.keep_alive());
}
#[test]
fn test_conn_default_1_1() {
let mut buf = Buffer::new("GET /test HTTP/1.1\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert!(req.keep_alive());
}
#[test]
fn test_conn_close() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
connection: close\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert!(!req.keep_alive());
}
#[test]
fn test_conn_close_1_0() {
let mut buf = Buffer::new(
"GET /test HTTP/1.0\r\n\
connection: close\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert!(!req.keep_alive());
}
#[test]
fn test_conn_keep_alive_1_0() {
let mut buf = Buffer::new(
"GET /test HTTP/1.0\r\n\
connection: keep-alive\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert!(req.keep_alive());
}
#[test]
fn test_conn_keep_alive_1_1() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
connection: keep-alive\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert!(req.keep_alive());
}
#[test]
fn test_conn_other_1_0() {
let mut buf = Buffer::new(
"GET /test HTTP/1.0\r\n\
connection: other\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert!(!req.keep_alive());
}
#[test]
fn test_conn_other_1_1() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
connection: other\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert!(req.keep_alive());
}
#[test]
fn test_conn_upgrade() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
connection: upgrade\r\n\r\n");
let (req, payload) = parse_ready!(&mut buf);
assert!(!payload.eof());
assert!(req.upgrade());
}
#[test]
fn test_conn_upgrade_connect_method() {
let mut buf = Buffer::new(
"CONNECT /test HTTP/1.1\r\n\
content-length: 0\r\n\r\n");
let (req, payload) = parse_ready!(&mut buf);
assert!(req.upgrade());
assert!(!payload.eof());
}
#[test]
fn test_request_chunked() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n");
let (req, payload) = parse_ready!(&mut buf);
assert!(!payload.eof());
if let Ok(val) = req.chunked() {
assert!(val);
} else {
panic!("Error");
}
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chnked\r\n\r\n");
let (req, payload) = parse_ready!(&mut buf);
assert!(payload.eof());
if let Ok(val) = req.chunked() {
assert!(!val);
} else {
panic!("Error");
}
}
#[test]
fn test_headers_content_length_err_1() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
content-length: line\r\n\r\n");
expect_parse_err!(&mut buf)
}
#[test]
fn test_headers_content_length_err_2() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
content-length: -1\r\n\r\n");
expect_parse_err!(&mut buf);
}
#[test]
fn test_invalid_header() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
test line\r\n\r\n");
expect_parse_err!(&mut buf);
}
#[test]
fn test_invalid_name() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
test[]: line\r\n\r\n");
expect_parse_err!(&mut buf);
}
#[test]
fn test_http_request_bad_status_line() {
let mut buf = Buffer::new("getpath \r\n\r\n");
expect_parse_err!(&mut buf);
}
#[test]
fn test_http_request_upgrade() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
connection: upgrade\r\n\
upgrade: websocket\r\n\r\n\
some raw data");
let (req, mut payload) = parse_ready!(&mut buf);
assert!(!req.keep_alive());
assert!(req.upgrade());
assert_eq!(payload.readall().unwrap().as_ref(), b"some raw data");
}
#[test]
fn test_http_request_parser_utf8() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
x-test: тест\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert_eq!(req.headers().get("x-test").unwrap().as_bytes(),
"тест".as_bytes());
}
#[test]
fn test_http_request_parser_two_slashes() {
let mut buf = Buffer::new(
"GET //path HTTP/1.1\r\n\r\n");
let (req, _) = parse_ready!(&mut buf);
assert_eq!(req.path(), "//path");
}
#[test]
fn test_http_request_parser_bad_method() {
let mut buf = Buffer::new(
"!12%()+=~$ /get HTTP/1.1\r\n\r\n");
expect_parse_err!(&mut buf);
}
#[test]
fn test_http_request_parser_bad_version() {
let mut buf = Buffer::new("GET //get HT/11\r\n\r\n");
expect_parse_err!(&mut buf);
}
#[test]
fn test_http_request_chunked_payload() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n");
let mut reader = Reader::new();
let (req, mut payload) = reader_parse_ready!(reader.parse(&mut buf));
assert!(req.chunked().unwrap());
assert!(!payload.eof());
buf.feed_data("4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n");
not_ready!(reader.parse(&mut buf));
assert!(!payload.eof());
assert_eq!(payload.readall().unwrap().as_ref(), b"dataline");
assert!(payload.eof());
}
#[test]
fn test_http_request_chunked_payload_and_next_message() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n");
let mut reader = Reader::new();
let (req, mut payload) = reader_parse_ready!(reader.parse(&mut buf));
assert!(req.chunked().unwrap());
assert!(!payload.eof());
buf.feed_data(
"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\
POST /test2 HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n");
let (req2, payload2) = reader_parse_ready!(reader.parse(&mut buf));
assert_eq!(*req2.method(), Method::POST);
assert!(req2.chunked().unwrap());
assert!(!payload2.eof());
assert_eq!(payload.readall().unwrap().as_ref(), b"dataline");
assert!(payload.eof());
}
#[test]
fn test_http_request_chunked_payload_chunks() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n");
let mut reader = Reader::new();
let (req, mut payload) = reader_parse_ready!(reader.parse(&mut buf));
assert!(req.chunked().unwrap());
assert!(!payload.eof());
buf.feed_data("4\r\ndata\r");
not_ready!(reader.parse(&mut buf));
buf.feed_data("\n4");
not_ready!(reader.parse(&mut buf));
buf.feed_data("\r");
not_ready!(reader.parse(&mut buf));
buf.feed_data("\n");
not_ready!(reader.parse(&mut buf));
buf.feed_data("li");
not_ready!(reader.parse(&mut buf));
buf.feed_data("ne\r\n0\r\n");
not_ready!(reader.parse(&mut buf));
//buf.feed_data("test: test\r\n");
//not_ready!(reader.parse(&mut buf));
assert_eq!(payload.readall().unwrap().as_ref(), b"dataline");
assert!(!payload.eof());
buf.feed_data("\r\n");
not_ready!(reader.parse(&mut buf));
assert!(payload.eof());
}
#[test]
fn test_parse_chunked_payload_chunk_extension() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n");
let mut reader = Reader::new();
let (req, mut payload) = reader_parse_ready!(reader.parse(&mut buf));
assert!(req.chunked().unwrap());
assert!(!payload.eof());
buf.feed_data("4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n")
not_ready!(reader.parse(&mut buf));
assert!(!payload.eof());
assert_eq!(payload.readall().unwrap().as_ref(), b"dataline");
assert!(payload.eof());
}
/*#[test]
#[should_panic]
fn test_parse_multiline() {
let mut buf = Buffer::new(
"GET /test HTTP/1.1\r\n\
test: line\r\n \
continue\r\n\
test2: data\r\n\
\r\n", false);
let mut reader = Reader::new();
match reader.parse(&mut buf) {
Ok(res) => (),
Err(err) => panic!("{:?}", err),
}
}*/
}

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