mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-03 09:36:36 +02:00
Compare commits
67 Commits
http-v2.0.
...
web-v3.2.0
Author | SHA1 | Date | |
---|---|---|---|
42f51eb962 | |||
156c97cef2 | |||
798d744eef | |||
4cb833616a | |||
9963a5ef54 | |||
4519db36b2 | |||
7030bf5fe8 | |||
20078fe603 | |||
06e5042b94 | |||
41e7cec72f | |||
d45a1aa6b6 | |||
98243db9f1 | |||
f92742bdac | |||
e563025b16 | |||
cfd5b381f1 | |||
2f84914146 | |||
d765e9099d | |||
34b23f31c9 | |||
26c1a901d9 | |||
c2c71cc626 | |||
aa11231ee5 | |||
b5812b15f0 | |||
b4e02fe29a | |||
37c76a39ab | |||
60e7e52276 | |||
c53e9468bc | |||
162121bf8d | |||
f7bcad9567 | |||
f9e3f78e45 | |||
1596893ef7 | |||
2a2474ca09 | |||
509b2e6eec | |||
d707704556 | |||
a429ee6646 | |||
7f8073233a | |||
4b4c9d1b93 | |||
3fde3be3d8 | |||
f861508789 | |||
a4546f02d2 | |||
64a2c13cdf | |||
bf53fe5a22 | |||
cf5138e740 | |||
121075c1ef | |||
22089aff87 | |||
7787638f26 | |||
2f6e9738c4 | |||
e39d166a17 | |||
059d1671d7 | |||
3a27580ebe | |||
9d0534999d | |||
c54d73e0bb | |||
9a9d4b182e | |||
4e321595bc | |||
01cbef700f | |||
8497b5f490 | |||
75d86a6beb | |||
3892a95c11 | |||
5802eb797f | |||
ff2ca0f420 | |||
59ad1738e9 | |||
aa2bd6fbfb | |||
5aad8e24c7 | |||
6e97bc09f8 | |||
160995b8d4 | |||
187646b2f9 | |||
46627be36f | |||
a78380739e |
2
.github/workflows/bench.yml
vendored
2
.github/workflows/bench.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: macOS-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
12
.github/workflows/upload-doc.yml
vendored
12
.github/workflows/upload-doc.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
if: github.repository == 'actix/actix-web'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
@ -29,7 +29,9 @@ jobs:
|
||||
- name: Tweak HTML
|
||||
run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html
|
||||
|
||||
- name: Upload documentation
|
||||
run: |
|
||||
git clone https://github.com/davisp/ghp-import.git
|
||||
./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://${{ secrets.GITHUB_TOKEN }}@github.com/"${{ github.repository }}.git" target/doc
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@3.5.8
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: target/doc
|
||||
|
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -9,6 +9,10 @@ guide/build/
|
||||
*.pid
|
||||
*.sock
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Configuration directory generated by CLion
|
||||
.idea
|
||||
|
92
CHANGES.md
92
CHANGES.md
@ -1,11 +1,95 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 3.2.0 - 2020-10-30
|
||||
### Added
|
||||
* Implement `exclude_regex` for Logger middleware. [#1723]
|
||||
* Add request-local data extractor `web::ReqData`. [#1748]
|
||||
* Add ability to register closure for request middleware logging. [#1749]
|
||||
* Add `app_data` to `ServiceConfig`. [#1757]
|
||||
* Expose `on_connect` for access to the connection stream before request is handled. [#1754]
|
||||
|
||||
### Changed
|
||||
* Updated actix-web-codegen dependency for access to new `#[route(...)]` multi-method macro.
|
||||
* Print non-configured `Data<T>` type when attempting extraction. [#1743]
|
||||
* Re-export bytes::Buf{Mut} in web module. [#1750]
|
||||
* Upgrade `pin-project` to `1.0`.
|
||||
|
||||
[#1723]: https://github.com/actix/actix-web/pull/1723
|
||||
[#1743]: https://github.com/actix/actix-web/pull/1743
|
||||
[#1748]: https://github.com/actix/actix-web/pull/1748
|
||||
[#1750]: https://github.com/actix/actix-web/pull/1750
|
||||
[#1754]: https://github.com/actix/actix-web/pull/1754
|
||||
[#1749]: https://github.com/actix/actix-web/pull/1749
|
||||
|
||||
|
||||
## 3.1.0 - 2020-09-29
|
||||
### Changed
|
||||
* Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath`
|
||||
to retain any trailing slashes. [#1695]
|
||||
* Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc<dyn Trait>`
|
||||
via `web::Data::from` [#1710]
|
||||
|
||||
### Fixed
|
||||
* `ResourceMap` debug printing is no longer infinitely recursive. [#1708]
|
||||
|
||||
[#1695]: https://github.com/actix/actix-web/pull/1695
|
||||
[#1708]: https://github.com/actix/actix-web/pull/1708
|
||||
[#1710]: https://github.com/actix/actix-web/pull/1710
|
||||
|
||||
|
||||
## 3.0.2 - 2020-09-15
|
||||
### Fixed
|
||||
* `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678]
|
||||
|
||||
[#1678]: https://github.com/actix/actix-web/pull/1678
|
||||
|
||||
|
||||
## 3.0.1 - 2020-09-13
|
||||
### Changed
|
||||
* `middleware::normalize::TrailingSlash` enum is now accessible. [#1673]
|
||||
|
||||
[#1673]: https://github.com/actix/actix-web/pull/1673
|
||||
|
||||
|
||||
## 3.0.0 - 2020-09-11
|
||||
* No significant changes from `3.0.0-beta.4`.
|
||||
|
||||
|
||||
## 3.0.0-beta.4 - 2020-09-09
|
||||
### Added
|
||||
* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing
|
||||
slash, or as the new addition, always trimming trailing slashes. [#1639]
|
||||
|
||||
### Changed
|
||||
* Update actix-codec and actix-utils dependencies. [#1634]
|
||||
* `FormConfig` and `JsonConfig` configurations are now also considered when set
|
||||
using `App::data`. [#1641]
|
||||
* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655]
|
||||
* `HttpServer::maxconnrate` is renamed to the more expressive
|
||||
`HttpServer::max_connection_rate`. [#1655]
|
||||
|
||||
[#1639]: https://github.com/actix/actix-web/pull/1639
|
||||
[#1641]: https://github.com/actix/actix-web/pull/1641
|
||||
[#1634]: https://github.com/actix/actix-web/pull/1634
|
||||
[#1655]: https://github.com/actix/actix-web/pull/1655
|
||||
|
||||
## 3.0.0-beta.3 - 2020-08-17
|
||||
### Changed
|
||||
* Update `rustls` to 0.18
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2020-08-17
|
||||
### Changed
|
||||
* `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set
|
||||
using `App::data`. [#1610]
|
||||
* `web::Path` now has a public representation: `web::Path(pub T)` that enables
|
||||
destructuring. [#1594]
|
||||
* `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to
|
||||
access `HttpRequest` which already allows this. [#1618]
|
||||
* Re-export all error types from `awc`. [#1621]
|
||||
* MSRV is now 1.42.0.
|
||||
|
||||
### Fixed
|
||||
@ -14,6 +98,8 @@
|
||||
[#1594]: https://github.com/actix/actix-web/pull/1594
|
||||
[#1609]: https://github.com/actix/actix-web/pull/1609
|
||||
[#1610]: https://github.com/actix/actix-web/pull/1610
|
||||
[#1618]: https://github.com/actix/actix-web/pull/1618
|
||||
[#1621]: https://github.com/actix/actix-web/pull/1621
|
||||
|
||||
|
||||
## 3.0.0-beta.1 - 2020-07-13
|
||||
@ -122,7 +208,7 @@
|
||||
|
||||
### Deleted
|
||||
|
||||
* Delete HttpServer::run(), it is not useful witht async/await
|
||||
* Delete HttpServer::run(), it is not useful with async/await
|
||||
|
||||
## [2.0.0-alpha.3] - 2019-12-07
|
||||
|
||||
@ -167,7 +253,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
* Make UrlEncodedError::Overflow more informativve
|
||||
* Make UrlEncodedError::Overflow more informative
|
||||
|
||||
* Use actix-testing for testing utils
|
||||
|
||||
@ -185,7 +271,7 @@
|
||||
|
||||
* Re-implement Host predicate (#989)
|
||||
|
||||
* Form immplements Responder, returning a `application/x-www-form-urlencoded` response
|
||||
* Form implements Responder, returning a `application/x-www-form-urlencoded` response
|
||||
|
||||
* Add `into_inner` to `Data`
|
||||
|
||||
|
41
Cargo.toml
41
Cargo.toml
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "3.0.0-beta.1"
|
||||
version = "3.2.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
|
||||
description = "Actix web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
homepage = "https://actix.rs"
|
||||
@ -64,21 +64,29 @@ required-features = ["compress"]
|
||||
name = "test_server"
|
||||
required-features = ["compress"]
|
||||
|
||||
[[example]]
|
||||
name = "on_connect"
|
||||
required-features = []
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
required-features = ["rustls"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.2.0"
|
||||
actix-service = "1.0.2"
|
||||
actix-utils = "1.0.6"
|
||||
actix-codec = "0.3.0"
|
||||
actix-service = "1.0.6"
|
||||
actix-utils = "2.0.0"
|
||||
actix-router = "0.2.4"
|
||||
actix-rt = "1.1.1"
|
||||
actix-server = "1.0.0"
|
||||
actix-testing = "1.0.0"
|
||||
actix-macros = "0.1.0"
|
||||
actix-threadpool = "0.3.1"
|
||||
actix-tls = "2.0.0-alpha.1"
|
||||
actix-tls = "2.0.0"
|
||||
|
||||
actix-web-codegen = "0.3.0-beta.1"
|
||||
actix-http = "2.0.0-alpha.4"
|
||||
awc = { version = "2.0.0-beta.1", default-features = false }
|
||||
actix-web-codegen = "0.4.0"
|
||||
actix-http = "2.1.0"
|
||||
awc = { version = "2.0.0", default-features = false }
|
||||
|
||||
bytes = "0.5.3"
|
||||
derive_more = "0.99.2"
|
||||
@ -90,21 +98,22 @@ fxhash = "0.2.1"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
socket2 = "0.3"
|
||||
pin-project = "0.4.17"
|
||||
regex = "1.3"
|
||||
serde = { version = "1.0", features=["derive"] }
|
||||
pin-project = "1.0.0"
|
||||
regex = "1.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.6.1"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
url = "2.1"
|
||||
open-ssl = { package = "openssl", version = "0.10", optional = true }
|
||||
rust-tls = { package = "rustls", version = "0.17.0", optional = true }
|
||||
tinyvec = { version = "0.3", features = ["alloc"] }
|
||||
rust-tls = { package = "rustls", version = "0.18.0", optional = true }
|
||||
tinyvec = { version = "1", features = ["alloc"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix = "0.10.0-alpha.1"
|
||||
actix = "0.10.0"
|
||||
actix-http = { version = "2.1.0", features = ["actors"] }
|
||||
rand = "0.7"
|
||||
env_logger = "0.7"
|
||||
env_logger = "0.8"
|
||||
serde_derive = "1.0"
|
||||
brotli2 = "0.3.2"
|
||||
flate2 = "1.0.13"
|
||||
|
@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-NOW Nikolay Kim
|
||||
Copyright 2017-NOW Actix Team
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017 Nikolay Kim
|
||||
Copyright (c) 2017 Actix Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
23
MIGRATION.md
23
MIGRATION.md
@ -1,11 +1,25 @@
|
||||
## Unreleased
|
||||
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
|
||||
simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
|
||||
|
||||
* Cookie handling has been offloaded to the `cookie` crate:
|
||||
* `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
||||
* Some types now require lifetime parameters.
|
||||
|
||||
* The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
|
||||
any `actix-web` method previously expecting a time v0.1 input.
|
||||
|
||||
* Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
||||
result in `SameSite=None` being sent with the response Set-Cookie header.
|
||||
To create a cookie without a SameSite attribute, remove any calls setting same_site.
|
||||
|
||||
* actix-http support for Actors messages was moved to actix-http crate and is enabled
|
||||
with feature `actors`
|
||||
|
||||
* content_length function is removed from actix-http.
|
||||
You can set Content-Length by normally setting the response body or calling no_chunking function.
|
||||
|
||||
@ -32,6 +46,15 @@
|
||||
}
|
||||
```
|
||||
|
||||
* `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
|
||||
It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`,
|
||||
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
|
||||
|
||||
* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.
|
||||
|
||||
* `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`.
|
||||
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
|
||||
|
13
README.md
13
README.md
@ -5,15 +5,17 @@
|
||||
</p>
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web)
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/3.2.0)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
|
||||

|
||||
[](https://deps.rs/crate/actix-web/2.2.0)
|
||||
<br />
|
||||
[](https://travis-ci.org/actix/actix-web)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
</p>
|
||||
</div>
|
||||
@ -37,17 +39,12 @@
|
||||
## Documentation
|
||||
|
||||
* [Website & User Guide](https://actix.rs)
|
||||
* [Examples Repository](https://actix.rs/actix-web/actix_web)
|
||||
* [Examples Repository](https://github.com/actix/examples)
|
||||
* [API Documentation](https://docs.rs/actix-web)
|
||||
* [API Documentation (master branch)](https://actix.rs/actix-web/actix_web)
|
||||
|
||||
## Example
|
||||
|
||||
<h2>
|
||||
WARNING: This example is for the master branch which is currently in beta stages for v3. For
|
||||
Actix web v2 see the <a href="https://actix.rs/docs/getting-started/">getting started guide</a>.
|
||||
</h2>
|
||||
|
||||
Dependencies:
|
||||
|
||||
```toml
|
||||
|
@ -1,11 +0,0 @@
|
||||
# Cors Middleware for actix web framework [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-cors) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
**This crate moved to https://github.com/actix/actix-extras.**
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-cors/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-cors](https://crates.io/crates/actix-cors)
|
||||
* Minimum supported Rust version: 1.34 or later
|
@ -1,12 +1,24 @@
|
||||
# Changes
|
||||
|
||||
## [Unreleased] - 2020-xx-xx
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
## [0.3.0-beta.1] - 2020-07-15
|
||||
|
||||
## 0.4.0 - 2020-10-06
|
||||
* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
|
||||
|
||||
[#1714]: https://github.com/actix/actix-web/pull/1714
|
||||
|
||||
|
||||
## 0.3.0 - 2020-09-11
|
||||
* No significant changes from 0.3.0-beta.1.
|
||||
|
||||
|
||||
## 0.3.0-beta.1 - 2020-07-15
|
||||
* Update `v_htmlescape` to 0.10
|
||||
* Update `actix-web` and `actix-http` dependencies to beta.1
|
||||
|
||||
## [0.3.0-alpha.1] - 2020-05-23
|
||||
|
||||
## 0.3.0-alpha.1 - 2020-05-23
|
||||
* Update `actix-web` and `actix-http` dependencies to alpha
|
||||
* Fix some typos in the docs
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
@ -14,77 +26,73 @@
|
||||
|
||||
[#1384]: https://github.com/actix/actix-web/pull/1384
|
||||
|
||||
## [0.2.1] - 2019-12-22
|
||||
|
||||
## 0.2.1 - 2019-12-22
|
||||
* Use the same format for file URLs regardless of platforms
|
||||
|
||||
## [0.2.0] - 2019-12-20
|
||||
|
||||
## 0.2.0 - 2019-12-20
|
||||
* Fix BodyEncoding trait import #1220
|
||||
|
||||
## [0.2.0-alpha.1] - 2019-12-07
|
||||
|
||||
## 0.2.0-alpha.1 - 2019-12-07
|
||||
* Migrate to `std::future`
|
||||
|
||||
## [0.1.7] - 2019-11-06
|
||||
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
|
||||
|
||||
## [0.1.6] - 2019-10-14
|
||||
## 0.1.7 - 2019-11-06
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of
|
||||
`actix_files::NamedFile` to be more compatible. (#1151)
|
||||
|
||||
## 0.1.6 - 2019-10-14
|
||||
* Add option to redirect to a slash-ended path `Files` #1132
|
||||
|
||||
## [0.1.5] - 2019-10-08
|
||||
|
||||
## 0.1.5 - 2019-10-08
|
||||
* Bump up `mime_guess` crate version to 2.0.1
|
||||
|
||||
* Bump up `percent-encoding` crate version to 2.1
|
||||
|
||||
* Allow user defined request guards for `Files` #1113
|
||||
|
||||
## [0.1.4] - 2019-07-20
|
||||
|
||||
## 0.1.4 - 2019-07-20
|
||||
* Allow to disable `Content-Disposition` header #686
|
||||
|
||||
## [0.1.3] - 2019-06-28
|
||||
|
||||
## 0.1.3 - 2019-06-28
|
||||
* Do not set `Content-Length` header, let actix-http set it #930
|
||||
|
||||
## [0.1.2] - 2019-06-13
|
||||
|
||||
## 0.1.2 - 2019-06-13
|
||||
* Content-Length is 0 for NamedFile HEAD request #914
|
||||
|
||||
* Fix ring dependency from actix-web default features for #741
|
||||
|
||||
## [0.1.1] - 2019-06-01
|
||||
|
||||
## 0.1.1 - 2019-06-01
|
||||
* Static files are incorrectly served as both chunked and with length #812
|
||||
|
||||
## [0.1.0] - 2019-05-25
|
||||
|
||||
* NamedFile last-modified check always fails due to nano-seconds
|
||||
in file modified date #820
|
||||
## 0.1.0 - 2019-05-25
|
||||
* NamedFile last-modified check always fails due to nano-seconds in file modified date #820
|
||||
|
||||
## [0.1.0-beta.4] - 2019-05-12
|
||||
|
||||
## 0.1.0-beta.4 - 2019-05-12
|
||||
* Update actix-web to beta.4
|
||||
|
||||
## [0.1.0-beta.1] - 2019-04-20
|
||||
|
||||
## 0.1.0-beta.1 - 2019-04-20
|
||||
* Update actix-web to beta.1
|
||||
|
||||
## [0.1.0-alpha.6] - 2019-04-14
|
||||
|
||||
## 0.1.0-alpha.6 - 2019-04-14
|
||||
* Update actix-web to alpha6
|
||||
|
||||
## [0.1.0-alpha.4] - 2019-04-08
|
||||
|
||||
## 0.1.0-alpha.4 - 2019-04-08
|
||||
* Update actix-web to alpha4
|
||||
|
||||
## [0.1.0-alpha.2] - 2019-04-02
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-04-02
|
||||
* Add default handler support
|
||||
|
||||
## [0.1.0-alpha.1] - 2019-03-28
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
* Initial impl
|
||||
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.3.0-beta.1"
|
||||
version = "0.4.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Static files support for actix web."
|
||||
description = "Static file serving for Actix Web"
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
@ -17,9 +17,8 @@ name = "actix_files"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "3.0.0-beta.1", default-features = false }
|
||||
actix-http = "2.0.0-beta.1"
|
||||
actix-service = "1.0.1"
|
||||
actix-web = { version = "3.0.0", default-features = false }
|
||||
actix-service = "1.0.6"
|
||||
bitflags = "1"
|
||||
bytes = "0.5.3"
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
@ -33,4 +32,4 @@ v_htmlescape = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-web = { version = "3.0.0-beta.1", features = ["openssl"] }
|
||||
actix-web = { version = "3.0.0", features = ["openssl"] }
|
||||
|
@ -1,9 +1,19 @@
|
||||
# Static files support for actix web [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-files) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# actix-files
|
||||
|
||||
## Documentation & community resources
|
||||
> Static file serving for Actix Web
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-files/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-files](https://crates.io/crates/actix-files)
|
||||
* Minimum supported Rust version: 1.40 or later
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.4.0)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-files/)
|
||||
- [Example Project](https://github.com/actix/examples/tree/master/static_index)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum supported Rust version: 1.42 or later
|
||||
|
94
actix-files/src/chunked.rs
Normal file
94
actix-files/src/chunked.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use std::{
|
||||
cmp, fmt,
|
||||
fs::File,
|
||||
future::Future,
|
||||
io::{self, Read, Seek},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_web::{
|
||||
error::{BlockingError, Error},
|
||||
web,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Stream};
|
||||
use futures_util::future::{FutureExt, LocalBoxFuture};
|
||||
|
||||
use crate::handle_error;
|
||||
|
||||
type ChunkedBoxFuture =
|
||||
LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>;
|
||||
|
||||
#[doc(hidden)]
|
||||
/// A helper created from a `std::fs::File` which reads the file
|
||||
/// chunk-by-chunk on a `ThreadPool`.
|
||||
pub struct ChunkedReadFile {
|
||||
pub(crate) size: u64,
|
||||
pub(crate) offset: u64,
|
||||
pub(crate) file: Option<File>,
|
||||
pub(crate) fut: Option<ChunkedBoxFuture>,
|
||||
pub(crate) counter: u64,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ChunkedReadFile {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("ChunkedReadFile")
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for ChunkedReadFile {
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
if let Some(ref mut fut) = self.fut {
|
||||
return match ready!(Pin::new(fut).poll(cx)) {
|
||||
Ok((file, bytes)) => {
|
||||
self.fut.take();
|
||||
self.file = Some(file);
|
||||
|
||||
self.offset += bytes.len() as u64;
|
||||
self.counter += bytes.len() as u64;
|
||||
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
Err(e) => Poll::Ready(Some(Err(handle_error(e)))),
|
||||
};
|
||||
}
|
||||
|
||||
let size = self.size;
|
||||
let offset = self.offset;
|
||||
let counter = self.counter;
|
||||
|
||||
if size == counter {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let mut file = self.file.take().expect("Use after completion");
|
||||
|
||||
self.fut = Some(
|
||||
web::block(move || {
|
||||
let max_bytes =
|
||||
cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
|
||||
let mut buf = Vec::with_capacity(max_bytes);
|
||||
file.seek(io::SeekFrom::Start(offset))?;
|
||||
|
||||
let n_bytes =
|
||||
file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
||||
|
||||
if n_bytes == 0 {
|
||||
return Err(io::ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
|
||||
Ok((file, Bytes::from(buf)))
|
||||
})
|
||||
.boxed_local(),
|
||||
);
|
||||
|
||||
self.poll_next(cx)
|
||||
}
|
||||
}
|
||||
}
|
114
actix-files/src/directory.rs
Normal file
114
actix-files/src/directory.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
|
||||
|
||||
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
|
||||
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
||||
use v_htmlescape::escape as escape_html_entity;
|
||||
|
||||
/// A directory; responds with the generated directory listing.
|
||||
#[derive(Debug)]
|
||||
pub struct Directory {
|
||||
/// Base directory.
|
||||
pub base: PathBuf,
|
||||
|
||||
/// Path of subdirectory to generate listing for.
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
/// Create a new directory
|
||||
pub fn new(base: PathBuf, path: PathBuf) -> Directory {
|
||||
Directory { base, path }
|
||||
}
|
||||
|
||||
/// Is this entry visible from this directory?
|
||||
pub fn is_visible(&self, entry: &io::Result<DirEntry>) -> bool {
|
||||
if let Ok(ref entry) = *entry {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.starts_with('.') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Ok(ref md) = entry.metadata() {
|
||||
let ft = md.file_type();
|
||||
return ft.is_dir() || ft.is_file() || ft.is_symlink();
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type DirectoryRenderer =
|
||||
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
|
||||
|
||||
// show file url as relative to static path
|
||||
macro_rules! encode_file_url {
|
||||
($path:ident) => {
|
||||
utf8_percent_encode(&$path, CONTROLS)
|
||||
};
|
||||
}
|
||||
|
||||
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
||||
macro_rules! encode_file_name {
|
||||
($entry:ident) => {
|
||||
escape_html_entity(&$entry.file_name().to_string_lossy())
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn directory_listing(
|
||||
dir: &Directory,
|
||||
req: &HttpRequest,
|
||||
) -> Result<ServiceResponse, io::Error> {
|
||||
let index_of = format!("Index of {}", req.path());
|
||||
let mut body = String::new();
|
||||
let base = Path::new(req.path());
|
||||
|
||||
for entry in dir.path.read_dir()? {
|
||||
if dir.is_visible(&entry) {
|
||||
let entry = entry.unwrap();
|
||||
let p = match entry.path().strip_prefix(&dir.path) {
|
||||
Ok(p) if cfg!(windows) => {
|
||||
base.join(p).to_string_lossy().replace("\\", "/")
|
||||
}
|
||||
Ok(p) => base.join(p).to_string_lossy().into_owned(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// if file is a directory, add '/' to the end of the name
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
if metadata.is_dir() {
|
||||
let _ = write!(
|
||||
body,
|
||||
"<li><a href=\"{}\">{}/</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
} else {
|
||||
let _ = write!(
|
||||
body,
|
||||
"<li><a href=\"{}\">{}</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let html = format!(
|
||||
"<html>\
|
||||
<head><title>{}</title></head>\
|
||||
<body><h1>{}</h1>\
|
||||
<ul>\
|
||||
{}\
|
||||
</ul></body>\n</html>",
|
||||
index_of, index_of, body
|
||||
);
|
||||
Ok(ServiceResponse::new(
|
||||
req.clone(),
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html),
|
||||
))
|
||||
}
|
52
actix-files/src/encoding.rs
Normal file
52
actix-files/src/encoding.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use mime::Mime;
|
||||
|
||||
/// Transforms MIME `text/*` types into their UTF-8 equivalent, if supported.
|
||||
///
|
||||
/// MIME types that are converted
|
||||
/// - application/javascript
|
||||
/// - text/html
|
||||
/// - text/css
|
||||
/// - text/plain
|
||||
/// - text/csv
|
||||
/// - text/tab-separated-values
|
||||
pub(crate) fn equiv_utf8_text(ct: Mime) -> Mime {
|
||||
// use (roughly) order of file-type popularity for a web server
|
||||
|
||||
if ct == mime::APPLICATION_JAVASCRIPT {
|
||||
return mime::APPLICATION_JAVASCRIPT_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_HTML {
|
||||
return mime::TEXT_HTML_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_CSS {
|
||||
return mime::TEXT_CSS_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_PLAIN {
|
||||
return mime::TEXT_PLAIN_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_CSV {
|
||||
return mime::TEXT_CSV_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_TAB_SEPARATED_VALUES {
|
||||
return mime::TEXT_TAB_SEPARATED_VALUES_UTF_8;
|
||||
}
|
||||
|
||||
ct
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_equiv_utf8_text() {
|
||||
assert_eq!(equiv_utf8_text(mime::TEXT_PLAIN), mime::TEXT_PLAIN_UTF_8);
|
||||
assert_eq!(equiv_utf8_text(mime::TEXT_XML), mime::TEXT_XML);
|
||||
assert_eq!(equiv_utf8_text(mime::IMAGE_PNG), mime::IMAGE_PNG);
|
||||
}
|
||||
}
|
259
actix-files/src/files.rs
Normal file
259
actix-files/src/files.rs
Normal file
@ -0,0 +1,259 @@
|
||||
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
|
||||
|
||||
use actix_service::{boxed, IntoServiceFactory, ServiceFactory};
|
||||
use actix_web::{
|
||||
dev::{
|
||||
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
|
||||
},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::header::DispositionType,
|
||||
HttpRequest,
|
||||
};
|
||||
use futures_util::future::{ok, FutureExt, LocalBoxFuture};
|
||||
|
||||
use crate::{
|
||||
directory_listing, named, Directory, DirectoryRenderer, FilesService,
|
||||
HttpNewService, MimeOverride,
|
||||
};
|
||||
|
||||
/// Static files handling service.
|
||||
///
|
||||
/// `Files` service must be registered with `App::service()` method.
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::App;
|
||||
/// use actix_files::Files;
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .service(Files::new("/static", "."));
|
||||
/// ```
|
||||
pub struct Files {
|
||||
path: String,
|
||||
directory: PathBuf,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
||||
renderer: Rc<DirectoryRenderer>,
|
||||
mime_override: Option<Rc<MimeOverride>>,
|
||||
file_flags: named::Flags,
|
||||
guards: Option<Rc<dyn Guard>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Files {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Files")
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Files {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
directory: self.directory.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
default: self.default.clone(),
|
||||
renderer: self.renderer.clone(),
|
||||
file_flags: self.file_flags,
|
||||
path: self.path.clone(),
|
||||
mime_override: self.mime_override.clone(),
|
||||
guards: self.guards.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Files {
|
||||
/// Create new `Files` instance for specified base directory.
|
||||
///
|
||||
/// `File` uses `ThreadPool` for blocking filesystem operations.
|
||||
/// By default pool with 5x threads of available cpus is used.
|
||||
/// Pool size can be changed by setting ACTIX_THREADPOOL environment variable.
|
||||
pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Files {
|
||||
let orig_dir = dir.into();
|
||||
let dir = match orig_dir.canonicalize() {
|
||||
Ok(canon_dir) => canon_dir,
|
||||
Err(_) => {
|
||||
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
||||
PathBuf::new()
|
||||
}
|
||||
};
|
||||
|
||||
Files {
|
||||
path: path.to_string(),
|
||||
directory: dir,
|
||||
index: None,
|
||||
show_index: false,
|
||||
redirect_to_slash: false,
|
||||
default: Rc::new(RefCell::new(None)),
|
||||
renderer: Rc::new(directory_listing),
|
||||
mime_override: None,
|
||||
file_flags: named::Flags::default(),
|
||||
guards: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show files listing for directories.
|
||||
///
|
||||
/// By default show files listing is disabled.
|
||||
pub fn show_files_listing(mut self) -> Self {
|
||||
self.show_index = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Redirects to a slash-ended path when browsing a directory.
|
||||
///
|
||||
/// By default never redirect.
|
||||
pub fn redirect_to_slash_directory(mut self) -> Self {
|
||||
self.redirect_to_slash = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set custom directory renderer
|
||||
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
|
||||
where
|
||||
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
|
||||
+ 'static,
|
||||
{
|
||||
self.renderer = Rc::new(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies mime override callback
|
||||
pub fn mime_override<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
|
||||
{
|
||||
self.mime_override = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index file
|
||||
///
|
||||
/// Shows specific index file for directory "/" instead of
|
||||
/// showing files listing.
|
||||
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
|
||||
self.index = Some(index.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use ETag or not.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
pub fn use_etag(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::ETAG, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use Last-Modified or not.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
pub fn use_last_modified(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::LAST_MD, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether text responses should signal a UTF-8 encoding.
|
||||
///
|
||||
/// Default is false (but will default to true in a future version).
|
||||
#[inline]
|
||||
pub fn prefer_utf8(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::PREFER_UTF8, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies custom guards to use for directory listings and files.
|
||||
///
|
||||
/// Default behaviour allows GET and HEAD.
|
||||
#[inline]
|
||||
pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self {
|
||||
self.guards = Some(Rc::new(guards));
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable `Content-Disposition` header.
|
||||
///
|
||||
/// By default Content-Disposition` header is enabled.
|
||||
#[inline]
|
||||
pub fn disable_content_disposition(mut self) -> Self {
|
||||
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets default handler which is used when no matched file could be found.
|
||||
pub fn default_handler<F, U>(mut self, f: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<U>,
|
||||
U: ServiceFactory<
|
||||
Config = (),
|
||||
Request = ServiceRequest,
|
||||
Response = ServiceResponse,
|
||||
Error = Error,
|
||||
> + 'static,
|
||||
{
|
||||
// create and configure default resource
|
||||
self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
|
||||
f.into_factory().map_init_err(|_| ()),
|
||||
)))));
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpServiceFactory for Files {
|
||||
fn register(self, config: &mut AppService) {
|
||||
if self.default.borrow().is_none() {
|
||||
*self.default.borrow_mut() = Some(config.default_service());
|
||||
}
|
||||
|
||||
let rdef = if config.is_root() {
|
||||
ResourceDef::root_prefix(&self.path)
|
||||
} else {
|
||||
ResourceDef::prefix(&self.path)
|
||||
};
|
||||
|
||||
config.register_service(rdef, None, self, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceFactory for Files {
|
||||
type Request = ServiceRequest;
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Config = ();
|
||||
type Service = FilesService;
|
||||
type InitError = ();
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let mut srv = FilesService {
|
||||
directory: self.directory.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
default: None,
|
||||
renderer: self.renderer.clone(),
|
||||
mime_override: self.mime_override.clone(),
|
||||
file_flags: self.file_flags,
|
||||
guards: self.guards.clone(),
|
||||
};
|
||||
|
||||
if let Some(ref default) = *self.default.borrow() {
|
||||
default
|
||||
.new_service(())
|
||||
.map(move |result| match result {
|
||||
Ok(default) => {
|
||||
srv.default = Some(default);
|
||||
Ok(srv)
|
||||
}
|
||||
Err(_) => Err(()),
|
||||
})
|
||||
.boxed_local()
|
||||
} else {
|
||||
ok(srv).boxed_local()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,42 +1,49 @@
|
||||
#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)]
|
||||
//! Static file serving for Actix Web.
|
||||
//!
|
||||
//! Provides a non-blocking service for serving static files from disk.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust
|
||||
//! use actix_web::App;
|
||||
//! use actix_files::Files;
|
||||
//!
|
||||
//! let app = App::new()
|
||||
//! .service(Files::new("/static", ".").prefer_utf8(true));
|
||||
//! ```
|
||||
|
||||
//! Static files support
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Write;
|
||||
use std::fs::{DirEntry, File};
|
||||
use std::future::Future;
|
||||
use std::io::{Read, Seek};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{cmp, io};
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
use actix_service::boxed::{self, BoxService, BoxServiceFactory};
|
||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||
use actix_web::dev::{
|
||||
AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest,
|
||||
ServiceResponse,
|
||||
use std::io;
|
||||
|
||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||
use actix_web::{
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
error::{BlockingError, Error, ErrorInternalServerError},
|
||||
http::header::DispositionType,
|
||||
};
|
||||
use actix_web::error::{BlockingError, Error, ErrorInternalServerError};
|
||||
use actix_web::guard::Guard;
|
||||
use actix_web::http::header::{self, DispositionType};
|
||||
use actix_web::http::Method;
|
||||
use actix_web::{web, FromRequest, HttpRequest, HttpResponse};
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use futures_util::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready};
|
||||
use mime_guess::from_ext;
|
||||
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
||||
use v_htmlescape::escape as escape_html_entity;
|
||||
|
||||
mod chunked;
|
||||
mod directory;
|
||||
mod encoding;
|
||||
mod error;
|
||||
mod files;
|
||||
mod named;
|
||||
mod path_buf;
|
||||
mod range;
|
||||
mod service;
|
||||
|
||||
use self::error::{FilesError, UriSegmentError};
|
||||
pub use crate::chunked::ChunkedReadFile;
|
||||
pub use crate::directory::Directory;
|
||||
pub use crate::files::Files;
|
||||
pub use crate::named::NamedFile;
|
||||
pub use crate::range::HttpRange;
|
||||
pub use crate::service::FilesService;
|
||||
|
||||
use self::directory::{directory_listing, DirectoryRenderer};
|
||||
use self::error::FilesError;
|
||||
use self::path_buf::PathBufWrap;
|
||||
|
||||
type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>;
|
||||
type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
|
||||
@ -49,614 +56,43 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
|
||||
from_ext(ext).first_or_octet_stream()
|
||||
}
|
||||
|
||||
fn handle_error(err: BlockingError<io::Error>) -> Error {
|
||||
pub(crate) fn handle_error(err: BlockingError<io::Error>) -> Error {
|
||||
match err {
|
||||
BlockingError::Error(err) => err.into(),
|
||||
BlockingError::Canceled => ErrorInternalServerError("Unexpected error"),
|
||||
}
|
||||
}
|
||||
#[doc(hidden)]
|
||||
/// A helper created from a `std::fs::File` which reads the file
|
||||
/// chunk-by-chunk on a `ThreadPool`.
|
||||
pub struct ChunkedReadFile {
|
||||
size: u64,
|
||||
offset: u64,
|
||||
file: Option<File>,
|
||||
fut:
|
||||
Option<LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>>,
|
||||
counter: u64,
|
||||
}
|
||||
|
||||
impl Stream for ChunkedReadFile {
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
if let Some(ref mut fut) = self.fut {
|
||||
return match Pin::new(fut).poll(cx) {
|
||||
Poll::Ready(Ok((file, bytes))) => {
|
||||
self.fut.take();
|
||||
self.file = Some(file);
|
||||
self.offset += bytes.len() as u64;
|
||||
self.counter += bytes.len() as u64;
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(handle_error(e)))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
};
|
||||
}
|
||||
|
||||
let size = self.size;
|
||||
let offset = self.offset;
|
||||
let counter = self.counter;
|
||||
|
||||
if size == counter {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let mut file = self.file.take().expect("Use after completion");
|
||||
self.fut = Some(
|
||||
web::block(move || {
|
||||
let max_bytes: usize;
|
||||
max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
let mut buf = Vec::with_capacity(max_bytes);
|
||||
file.seek(io::SeekFrom::Start(offset))?;
|
||||
let nbytes =
|
||||
file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
||||
if nbytes == 0 {
|
||||
return Err(io::ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
Ok((file, Bytes::from(buf)))
|
||||
})
|
||||
.boxed_local(),
|
||||
);
|
||||
self.poll_next(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DirectoryRenderer =
|
||||
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
|
||||
|
||||
/// A directory; responds with the generated directory listing.
|
||||
#[derive(Debug)]
|
||||
pub struct Directory {
|
||||
/// Base directory
|
||||
pub base: PathBuf,
|
||||
/// Path of subdirectory to generate listing for
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
/// Create a new directory
|
||||
pub fn new(base: PathBuf, path: PathBuf) -> Directory {
|
||||
Directory { base, path }
|
||||
}
|
||||
|
||||
/// Is this entry visible from this directory?
|
||||
pub fn is_visible(&self, entry: &io::Result<DirEntry>) -> bool {
|
||||
if let Ok(ref entry) = *entry {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.starts_with('.') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Ok(ref md) = entry.metadata() {
|
||||
let ft = md.file_type();
|
||||
return ft.is_dir() || ft.is_file() || ft.is_symlink();
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// show file url as relative to static path
|
||||
macro_rules! encode_file_url {
|
||||
($path:ident) => {
|
||||
utf8_percent_encode(&$path, CONTROLS)
|
||||
};
|
||||
}
|
||||
|
||||
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
||||
macro_rules! encode_file_name {
|
||||
($entry:ident) => {
|
||||
escape_html_entity(&$entry.file_name().to_string_lossy())
|
||||
};
|
||||
}
|
||||
|
||||
fn directory_listing(
|
||||
dir: &Directory,
|
||||
req: &HttpRequest,
|
||||
) -> Result<ServiceResponse, io::Error> {
|
||||
let index_of = format!("Index of {}", req.path());
|
||||
let mut body = String::new();
|
||||
let base = Path::new(req.path());
|
||||
|
||||
for entry in dir.path.read_dir()? {
|
||||
if dir.is_visible(&entry) {
|
||||
let entry = entry.unwrap();
|
||||
let p = match entry.path().strip_prefix(&dir.path) {
|
||||
Ok(p) if cfg!(windows) => {
|
||||
base.join(p).to_string_lossy().replace("\\", "/")
|
||||
}
|
||||
Ok(p) => base.join(p).to_string_lossy().into_owned(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// if file is a directory, add '/' to the end of the name
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
if metadata.is_dir() {
|
||||
let _ = write!(
|
||||
body,
|
||||
"<li><a href=\"{}\">{}/</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
} else {
|
||||
let _ = write!(
|
||||
body,
|
||||
"<li><a href=\"{}\">{}</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let html = format!(
|
||||
"<html>\
|
||||
<head><title>{}</title></head>\
|
||||
<body><h1>{}</h1>\
|
||||
<ul>\
|
||||
{}\
|
||||
</ul></body>\n</html>",
|
||||
index_of, index_of, body
|
||||
);
|
||||
Ok(ServiceResponse::new(
|
||||
req.clone(),
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html),
|
||||
))
|
||||
}
|
||||
|
||||
type MimeOverride = dyn Fn(&mime::Name) -> DispositionType;
|
||||
|
||||
/// Static files handling
|
||||
///
|
||||
/// `Files` service must be registered with `App::service()` method.
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::App;
|
||||
/// use actix_files as fs;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = App::new()
|
||||
/// .service(fs::Files::new("/static", "."));
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Files {
|
||||
path: String,
|
||||
directory: PathBuf,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
||||
renderer: Rc<DirectoryRenderer>,
|
||||
mime_override: Option<Rc<MimeOverride>>,
|
||||
file_flags: named::Flags,
|
||||
// FIXME: Should re-visit later.
|
||||
#[allow(clippy::redundant_allocation)]
|
||||
guards: Option<Rc<Box<dyn Guard>>>,
|
||||
}
|
||||
|
||||
impl Clone for Files {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
directory: self.directory.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
default: self.default.clone(),
|
||||
renderer: self.renderer.clone(),
|
||||
file_flags: self.file_flags,
|
||||
path: self.path.clone(),
|
||||
mime_override: self.mime_override.clone(),
|
||||
guards: self.guards.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Files {
|
||||
/// Create new `Files` instance for specified base directory.
|
||||
///
|
||||
/// `File` uses `ThreadPool` for blocking filesystem operations.
|
||||
/// By default pool with 5x threads of available cpus is used.
|
||||
/// Pool size can be changed by setting ACTIX_THREADPOOL environment variable.
|
||||
pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Files {
|
||||
let orig_dir = dir.into();
|
||||
let dir = match orig_dir.canonicalize() {
|
||||
Ok(canon_dir) => canon_dir,
|
||||
Err(_) => {
|
||||
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
||||
PathBuf::new()
|
||||
}
|
||||
};
|
||||
|
||||
Files {
|
||||
path: path.to_string(),
|
||||
directory: dir,
|
||||
index: None,
|
||||
show_index: false,
|
||||
redirect_to_slash: false,
|
||||
default: Rc::new(RefCell::new(None)),
|
||||
renderer: Rc::new(directory_listing),
|
||||
mime_override: None,
|
||||
file_flags: named::Flags::default(),
|
||||
guards: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show files listing for directories.
|
||||
///
|
||||
/// By default show files listing is disabled.
|
||||
pub fn show_files_listing(mut self) -> Self {
|
||||
self.show_index = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Redirects to a slash-ended path when browsing a directory.
|
||||
///
|
||||
/// By default never redirect.
|
||||
pub fn redirect_to_slash_directory(mut self) -> Self {
|
||||
self.redirect_to_slash = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set custom directory renderer
|
||||
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
|
||||
where
|
||||
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
|
||||
+ 'static,
|
||||
{
|
||||
self.renderer = Rc::new(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies mime override callback
|
||||
pub fn mime_override<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mime::Name) -> DispositionType + 'static,
|
||||
{
|
||||
self.mime_override = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index file
|
||||
///
|
||||
/// Shows specific index file for directory "/" instead of
|
||||
/// showing files listing.
|
||||
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
|
||||
self.index = Some(index.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Specifies whether to use ETag or not.
|
||||
///
|
||||
/// Default is true.
|
||||
pub fn use_etag(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::ETAG, value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Specifies whether to use Last-Modified or not.
|
||||
///
|
||||
/// Default is true.
|
||||
pub fn use_last_modified(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::LAST_MD, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies custom guards to use for directory listings and files.
|
||||
///
|
||||
/// Default behaviour allows GET and HEAD.
|
||||
#[inline]
|
||||
pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self {
|
||||
self.guards = Some(Rc::new(Box::new(guards)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable `Content-Disposition` header.
|
||||
///
|
||||
/// By default Content-Disposition` header is enabled.
|
||||
#[inline]
|
||||
pub fn disable_content_disposition(mut self) -> Self {
|
||||
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets default handler which is used when no matched file could be found.
|
||||
pub fn default_handler<F, U>(mut self, f: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<U>,
|
||||
U: ServiceFactory<
|
||||
Config = (),
|
||||
Request = ServiceRequest,
|
||||
Response = ServiceResponse,
|
||||
Error = Error,
|
||||
> + 'static,
|
||||
{
|
||||
// create and configure default resource
|
||||
self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
|
||||
f.into_factory().map_init_err(|_| ()),
|
||||
)))));
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpServiceFactory for Files {
|
||||
fn register(self, config: &mut AppService) {
|
||||
if self.default.borrow().is_none() {
|
||||
*self.default.borrow_mut() = Some(config.default_service());
|
||||
}
|
||||
let rdef = if config.is_root() {
|
||||
ResourceDef::root_prefix(&self.path)
|
||||
} else {
|
||||
ResourceDef::prefix(&self.path)
|
||||
};
|
||||
config.register_service(rdef, None, self, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceFactory for Files {
|
||||
type Request = ServiceRequest;
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Config = ();
|
||||
type Service = FilesService;
|
||||
type InitError = ();
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let mut srv = FilesService {
|
||||
directory: self.directory.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
default: None,
|
||||
renderer: self.renderer.clone(),
|
||||
mime_override: self.mime_override.clone(),
|
||||
file_flags: self.file_flags,
|
||||
guards: self.guards.clone(),
|
||||
};
|
||||
|
||||
if let Some(ref default) = *self.default.borrow() {
|
||||
default
|
||||
.new_service(())
|
||||
.map(move |result| match result {
|
||||
Ok(default) => {
|
||||
srv.default = Some(default);
|
||||
Ok(srv)
|
||||
}
|
||||
Err(_) => Err(()),
|
||||
})
|
||||
.boxed_local()
|
||||
} else {
|
||||
ok(srv).boxed_local()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FilesService {
|
||||
directory: PathBuf,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
default: Option<HttpService>,
|
||||
renderer: Rc<DirectoryRenderer>,
|
||||
mime_override: Option<Rc<MimeOverride>>,
|
||||
file_flags: named::Flags,
|
||||
// FIXME: Should re-visit later.
|
||||
#[allow(clippy::redundant_allocation)]
|
||||
guards: Option<Rc<Box<dyn Guard>>>,
|
||||
}
|
||||
|
||||
impl FilesService {
|
||||
fn handle_err(
|
||||
&mut self,
|
||||
e: io::Error,
|
||||
req: ServiceRequest,
|
||||
) -> Either<
|
||||
Ready<Result<ServiceResponse, Error>>,
|
||||
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
|
||||
> {
|
||||
log::debug!("Files: Failed to handle {}: {}", req.path(), e);
|
||||
if let Some(ref mut default) = self.default {
|
||||
Either::Right(default.call(req))
|
||||
} else {
|
||||
Either::Left(ok(req.error_response(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for FilesService {
|
||||
type Request = ServiceRequest;
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Future = Either<
|
||||
Ready<Result<Self::Response, Self::Error>>,
|
||||
LocalBoxFuture<'static, Result<Self::Response, Self::Error>>,
|
||||
>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: ServiceRequest) -> Self::Future {
|
||||
let is_method_valid = if let Some(guard) = &self.guards {
|
||||
// execute user defined guards
|
||||
(**guard).check(req.head())
|
||||
} else {
|
||||
// default behavior
|
||||
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||
};
|
||||
|
||||
if !is_method_valid {
|
||||
return Either::Left(ok(req.into_response(
|
||||
actix_web::HttpResponse::MethodNotAllowed()
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body("Request did not meet this resource's requirements."),
|
||||
)));
|
||||
}
|
||||
|
||||
let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Either::Left(ok(req.error_response(e))),
|
||||
};
|
||||
|
||||
// full file path
|
||||
let path = match self.directory.join(&real_path.0).canonicalize() {
|
||||
Ok(path) => path,
|
||||
Err(e) => return self.handle_err(e, req),
|
||||
};
|
||||
|
||||
if path.is_dir() {
|
||||
if let Some(ref redir_index) = self.index {
|
||||
if self.redirect_to_slash && !req.path().ends_with('/') {
|
||||
let redirect_to = format!("{}/", req.path());
|
||||
return Either::Left(ok(req.into_response(
|
||||
HttpResponse::Found()
|
||||
.header(header::LOCATION, redirect_to)
|
||||
.body("")
|
||||
.into_body(),
|
||||
)));
|
||||
}
|
||||
|
||||
let path = path.join(redir_index);
|
||||
|
||||
match NamedFile::open(path) {
|
||||
Ok(mut named_file) => {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition =
|
||||
mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
|
||||
named_file.flags = self.file_flags;
|
||||
let (req, _) = req.into_parts();
|
||||
Either::Left(ok(match named_file.into_response(&req) {
|
||||
Ok(item) => ServiceResponse::new(req, item),
|
||||
Err(e) => ServiceResponse::from_err(e, req),
|
||||
}))
|
||||
}
|
||||
Err(e) => self.handle_err(e, req),
|
||||
}
|
||||
} else if self.show_index {
|
||||
let dir = Directory::new(self.directory.clone(), path);
|
||||
let (req, _) = req.into_parts();
|
||||
let x = (self.renderer)(&dir, &req);
|
||||
match x {
|
||||
Ok(resp) => Either::Left(ok(resp)),
|
||||
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
|
||||
}
|
||||
} else {
|
||||
Either::Left(ok(ServiceResponse::from_err(
|
||||
FilesError::IsDirectory,
|
||||
req.into_parts().0,
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
match NamedFile::open(path) {
|
||||
Ok(mut named_file) => {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition =
|
||||
mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
|
||||
named_file.flags = self.file_flags;
|
||||
let (req, _) = req.into_parts();
|
||||
match named_file.into_response(&req) {
|
||||
Ok(item) => {
|
||||
Either::Left(ok(ServiceResponse::new(req.clone(), item)))
|
||||
}
|
||||
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
|
||||
}
|
||||
}
|
||||
Err(e) => self.handle_err(e, req),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PathBufWrp(PathBuf);
|
||||
|
||||
impl PathBufWrp {
|
||||
fn get_pathbuf(path: &str) -> Result<Self, UriSegmentError> {
|
||||
let mut buf = PathBuf::new();
|
||||
for segment in path.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(PathBufWrp(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for PathBufWrp {
|
||||
type Error = UriSegmentError;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
type Config = ();
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(PathBufWrp::get_pathbuf(req.match_info().path()))
|
||||
}
|
||||
}
|
||||
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
use std::iter::FromIterator;
|
||||
use std::ops::Add;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
ops::Add,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::{
|
||||
guard,
|
||||
http::{
|
||||
header::{self, ContentDisposition, DispositionParam, DispositionType},
|
||||
Method, StatusCode,
|
||||
},
|
||||
middleware::Compress,
|
||||
test::{self, TestRequest},
|
||||
web, App, HttpResponse, Responder,
|
||||
};
|
||||
use futures_util::future::ok;
|
||||
|
||||
use super::*;
|
||||
use actix_web::guard;
|
||||
use actix_web::http::header::{
|
||||
self, ContentDisposition, DispositionParam, DispositionType,
|
||||
};
|
||||
use actix_web::http::{Method, StatusCode};
|
||||
use actix_web::middleware::Compress;
|
||||
use actix_web::test::{self, TestRequest};
|
||||
use actix_web::{App, Responder};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_file_extension_to_mime() {
|
||||
let m = file_extension_to_mime("");
|
||||
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
|
||||
|
||||
let m = file_extension_to_mime("jpg");
|
||||
assert_eq!(m, mime::IMAGE_JPEG);
|
||||
|
||||
@ -898,7 +334,7 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_mime_override() {
|
||||
fn all_attachment(_: &mime::Name) -> DispositionType {
|
||||
fn all_attachment(_: &mime::Name<'_>) -> DispositionType {
|
||||
DispositionType::Attachment
|
||||
}
|
||||
|
||||
@ -1010,7 +446,7 @@ mod tests {
|
||||
|
||||
// Check file contents
|
||||
let bytes = response.body().await.unwrap();
|
||||
let data = Bytes::from(fs::read("tests/test.binary").unwrap());
|
||||
let data = web::Bytes::from(fs::read("tests/test.binary").unwrap());
|
||||
assert_eq!(bytes, data);
|
||||
}
|
||||
|
||||
@ -1043,7 +479,7 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let bytes = test::read_body(response).await;
|
||||
let data = Bytes::from(fs::read("tests/test space.binary").unwrap());
|
||||
let data = web::Bytes::from(fs::read("tests/test space.binary").unwrap());
|
||||
assert_eq!(bytes, data);
|
||||
}
|
||||
|
||||
@ -1221,7 +657,7 @@ mod tests {
|
||||
let resp = test::call_service(&mut st, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let bytes = test::read_body(resp).await;
|
||||
assert_eq!(bytes, Bytes::from_static(b"default content"));
|
||||
assert_eq!(bytes, web::Bytes::from_static(b"default content"));
|
||||
}
|
||||
|
||||
// #[actix_rt::test]
|
||||
@ -1337,36 +773,4 @@ mod tests {
|
||||
// let response = srv.execute(request.send()).unwrap();
|
||||
// assert_eq!(response.status(), StatusCode::OK);
|
||||
// }
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_path_buf() {
|
||||
assert_eq!(
|
||||
PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0),
|
||||
Err(UriSegmentError::BadStart('.'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0),
|
||||
Err(UriSegmentError::BadStart('*'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0),
|
||||
Err(UriSegmentError::BadEnd(':'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0),
|
||||
Err(UriSegmentError::BadEnd('<'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0),
|
||||
Err(UriSegmentError::BadEnd('>'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0,
|
||||
PathBuf::from_iter(vec!["seg1", "seg2"])
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0,
|
||||
PathBuf::from_iter(vec!["seg2"])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,32 +7,36 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use actix_web::{
|
||||
dev::{BodyEncoding, SizedStream},
|
||||
http::{
|
||||
header::{
|
||||
self, Charset, ContentDisposition, DispositionParam, DispositionType,
|
||||
ExtendedValue,
|
||||
},
|
||||
ContentEncoding, StatusCode,
|
||||
},
|
||||
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||
};
|
||||
use bitflags::bitflags;
|
||||
use futures_util::future::{ready, Ready};
|
||||
use mime_guess::from_path;
|
||||
|
||||
use actix_http::body::SizedStream;
|
||||
use actix_web::dev::BodyEncoding;
|
||||
use actix_web::http::header::{
|
||||
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
|
||||
};
|
||||
use actix_web::http::{ContentEncoding, StatusCode};
|
||||
use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder};
|
||||
use futures_util::future::{ready, Ready};
|
||||
|
||||
use crate::range::HttpRange;
|
||||
use crate::ChunkedReadFile;
|
||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||
|
||||
bitflags! {
|
||||
pub(crate) struct Flags: u8 {
|
||||
const ETAG = 0b0000_0001;
|
||||
const LAST_MD = 0b0000_0010;
|
||||
const ETAG = 0b0000_0001;
|
||||
const LAST_MD = 0b0000_0010;
|
||||
const CONTENT_DISPOSITION = 0b0000_0100;
|
||||
const PREFER_UTF8 = 0b0000_1000;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Flags {
|
||||
fn default() -> Self {
|
||||
Flags::all()
|
||||
Flags::from_bits_truncate(0b0000_0111)
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,12 +93,15 @@ impl NamedFile {
|
||||
};
|
||||
|
||||
let ct = from_path(&path).first_or_octet_stream();
|
||||
|
||||
let disposition = match ct.type_() {
|
||||
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
||||
_ => DispositionType::Attachment,
|
||||
};
|
||||
|
||||
let mut parameters =
|
||||
vec![DispositionParam::Filename(String::from(filename.as_ref()))];
|
||||
|
||||
if !filename.is_ascii() {
|
||||
parameters.push(DispositionParam::FilenameExt(ExtendedValue {
|
||||
charset: Charset::Ext(String::from("UTF-8")),
|
||||
@ -102,16 +109,19 @@ impl NamedFile {
|
||||
value: filename.into_owned().into_bytes(),
|
||||
}))
|
||||
}
|
||||
|
||||
let cd = ContentDisposition {
|
||||
disposition,
|
||||
parameters,
|
||||
};
|
||||
|
||||
(ct, cd)
|
||||
};
|
||||
|
||||
let md = file.metadata()?;
|
||||
let modified = md.modified().ok();
|
||||
let encoding = None;
|
||||
|
||||
Ok(NamedFile {
|
||||
path,
|
||||
file,
|
||||
@ -207,24 +217,33 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
///Specifies whether to use ETag or not.
|
||||
/// Specifies whether to use ETag or not.
|
||||
///
|
||||
///Default is true.
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
pub fn use_etag(mut self, value: bool) -> Self {
|
||||
self.flags.set(Flags::ETAG, value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
///Specifies whether to use Last-Modified or not.
|
||||
/// Specifies whether to use Last-Modified or not.
|
||||
///
|
||||
///Default is true.
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
pub fn use_last_modified(mut self, value: bool) -> Self {
|
||||
self.flags.set(Flags::LAST_MD, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether text responses should signal a UTF-8 encoding.
|
||||
///
|
||||
/// Default is false (but will default to true in a future version).
|
||||
#[inline]
|
||||
pub fn prefer_utf8(mut self, value: bool) -> Self {
|
||||
self.flags.set(Flags::PREFER_UTF8, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
||||
// This etag format is similar to Apache's.
|
||||
self.modified.as_ref().map(|mtime| {
|
||||
@ -242,6 +261,7 @@ impl NamedFile {
|
||||
let dur = mtime
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("modification time must be after epoch");
|
||||
|
||||
header::EntityTag::strong(format!(
|
||||
"{:x}:{:x}:{:x}:{:x}",
|
||||
ino,
|
||||
@ -256,19 +276,29 @@ impl NamedFile {
|
||||
self.modified.map(|mtime| mtime.into())
|
||||
}
|
||||
|
||||
/// Creates an `HttpResponse` with file as a streaming body.
|
||||
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
|
||||
if self.status_code != StatusCode::OK {
|
||||
let mut resp = HttpResponse::build(self.status_code);
|
||||
resp.set(header::ContentType(self.content_type.clone()))
|
||||
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
|
||||
res.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
});
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
resp.encoding(current_encoding);
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
res.header(header::CONTENT_TYPE, ct.to_string());
|
||||
} else {
|
||||
res.header(header::CONTENT_TYPE, self.content_type.to_string());
|
||||
}
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
res.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
res.encoding(current_encoding);
|
||||
}
|
||||
|
||||
let reader = ChunkedReadFile {
|
||||
size: self.md.len(),
|
||||
offset: 0,
|
||||
@ -276,7 +306,8 @@ impl NamedFile {
|
||||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
return Ok(resp.streaming(reader));
|
||||
|
||||
return Ok(res.streaming(reader));
|
||||
}
|
||||
|
||||
let etag = if self.flags.contains(Flags::ETAG) {
|
||||
@ -284,6 +315,7 @@ impl NamedFile {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let last_modified = if self.flags.contains(Flags::LAST_MD) {
|
||||
self.last_modified()
|
||||
} else {
|
||||
@ -298,6 +330,7 @@ impl NamedFile {
|
||||
{
|
||||
let t1: SystemTime = m.clone().into();
|
||||
let t2: SystemTime = since.clone().into();
|
||||
|
||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||
(Ok(t1), Ok(t2)) => t1 > t2,
|
||||
_ => false,
|
||||
@ -309,13 +342,14 @@ impl NamedFile {
|
||||
// check last modified
|
||||
let not_modified = if !none_match(etag.as_ref(), req) {
|
||||
true
|
||||
} else if req.headers().contains_key(&header::IF_NONE_MATCH) {
|
||||
} else if req.headers().contains_key(header::IF_NONE_MATCH) {
|
||||
false
|
||||
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
|
||||
(last_modified, req.get_header())
|
||||
{
|
||||
let t1: SystemTime = m.clone().into();
|
||||
let t2: SystemTime = since.clone().into();
|
||||
|
||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||
(Ok(t1), Ok(t2)) => t1 <= t2,
|
||||
_ => false,
|
||||
@ -325,24 +359,33 @@ impl NamedFile {
|
||||
};
|
||||
|
||||
let mut resp = HttpResponse::build(self.status_code);
|
||||
resp.set(header::ContentType(self.content_type.clone()))
|
||||
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
|
||||
res.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
});
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
resp.header(header::CONTENT_TYPE, ct.to_string());
|
||||
} else {
|
||||
resp.header(header::CONTENT_TYPE, self.content_type.to_string());
|
||||
}
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
resp.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// default compressing
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
resp.encoding(current_encoding);
|
||||
}
|
||||
|
||||
resp.if_some(last_modified, |lm, resp| {
|
||||
resp.set(header::LastModified(lm));
|
||||
})
|
||||
.if_some(etag, |etag, resp| {
|
||||
resp.set(header::ETag(etag));
|
||||
});
|
||||
if let Some(lm) = last_modified {
|
||||
resp.header(header::LAST_MODIFIED, lm.to_string());
|
||||
}
|
||||
|
||||
if let Some(etag) = etag {
|
||||
resp.header(header::ETAG, etag.to_string());
|
||||
}
|
||||
|
||||
resp.header(header::ACCEPT_RANGES, "bytes");
|
||||
|
||||
@ -350,11 +393,12 @@ impl NamedFile {
|
||||
let mut offset = 0;
|
||||
|
||||
// check for range header
|
||||
if let Some(ranges) = req.headers().get(&header::RANGE) {
|
||||
if let Ok(rangesheader) = ranges.to_str() {
|
||||
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
|
||||
length = rangesvec[0].length;
|
||||
offset = rangesvec[0].start;
|
||||
if let Some(ranges) = req.headers().get(header::RANGE) {
|
||||
if let Ok(ranges_header) = ranges.to_str() {
|
||||
if let Ok(ranges) = HttpRange::parse(ranges_header, length) {
|
||||
length = ranges[0].length;
|
||||
offset = ranges[0].start;
|
||||
|
||||
resp.encoding(ContentEncoding::Identity);
|
||||
resp.header(
|
||||
header::CONTENT_RANGE,
|
||||
@ -414,6 +458,7 @@ impl DerefMut for NamedFile {
|
||||
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
match req.get_header::<header::IfMatch>() {
|
||||
None | Some(header::IfMatch::Any) => true,
|
||||
|
||||
Some(header::IfMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
@ -422,6 +467,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -431,6 +477,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
match req.get_header::<header::IfNoneMatch>() {
|
||||
Some(header::IfNoneMatch::Any) => false,
|
||||
|
||||
Some(header::IfNoneMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
@ -439,8 +486,10 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
99
actix-files/src/path_buf.rs
Normal file
99
actix-files/src/path_buf.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use actix_web::{dev::Payload, FromRequest, HttpRequest};
|
||||
use futures_util::future::{ready, Ready};
|
||||
|
||||
use crate::error::UriSegmentError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PathBufWrap(PathBuf);
|
||||
|
||||
impl FromStr for PathBufWrap {
|
||||
type Err = UriSegmentError;
|
||||
|
||||
fn from_str(path: &str) -> Result<Self, Self::Err> {
|
||||
let mut buf = PathBuf::new();
|
||||
|
||||
for segment in path.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(PathBufWrap(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for PathBufWrap {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for PathBufWrap {
|
||||
type Error = UriSegmentError;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
type Config = ();
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(req.match_info().path().parse())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_path_buf() {
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/.tt").map(|t| t.0),
|
||||
Err(UriSegmentError::BadStart('.'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/*tt").map(|t| t.0),
|
||||
Err(UriSegmentError::BadStart('*'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/tt:").map(|t| t.0),
|
||||
Err(UriSegmentError::BadEnd(':'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/tt<").map(|t| t.0),
|
||||
Err(UriSegmentError::BadEnd('<'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/tt>").map(|t| t.0),
|
||||
Err(UriSegmentError::BadEnd('>'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/seg1/seg2/").unwrap().0,
|
||||
PathBuf::from_iter(vec!["seg1", "seg2"])
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/seg1/../seg2/").unwrap().0,
|
||||
PathBuf::from_iter(vec!["seg2"])
|
||||
);
|
||||
}
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
/// HTTP Range header representation.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct HttpRange {
|
||||
/// Start of range.
|
||||
pub start: u64,
|
||||
|
||||
/// Length of range.
|
||||
pub length: u64,
|
||||
}
|
||||
|
||||
static PREFIX: &str = "bytes=";
|
||||
const PREFIX: &str = "bytes=";
|
||||
const PREFIX_LEN: usize = 6;
|
||||
|
||||
impl HttpRange {
|
||||
|
167
actix-files/src/service.rs
Normal file
167
actix-files/src/service.rs
Normal file
@ -0,0 +1,167 @@
|
||||
use std::{
|
||||
fmt, io,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_service::Service;
|
||||
use actix_web::{
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::{header, Method},
|
||||
HttpResponse,
|
||||
};
|
||||
use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
|
||||
|
||||
use crate::{
|
||||
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride,
|
||||
NamedFile, PathBufWrap,
|
||||
};
|
||||
|
||||
/// Assembled file serving service.
|
||||
pub struct FilesService {
|
||||
pub(crate) directory: PathBuf,
|
||||
pub(crate) index: Option<String>,
|
||||
pub(crate) show_index: bool,
|
||||
pub(crate) redirect_to_slash: bool,
|
||||
pub(crate) default: Option<HttpService>,
|
||||
pub(crate) renderer: Rc<DirectoryRenderer>,
|
||||
pub(crate) mime_override: Option<Rc<MimeOverride>>,
|
||||
pub(crate) file_flags: named::Flags,
|
||||
pub(crate) guards: Option<Rc<dyn Guard>>,
|
||||
}
|
||||
|
||||
type FilesServiceFuture = Either<
|
||||
Ready<Result<ServiceResponse, Error>>,
|
||||
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
|
||||
>;
|
||||
|
||||
impl FilesService {
|
||||
fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
|
||||
log::debug!("Failed to handle {}: {}", req.path(), e);
|
||||
|
||||
if let Some(ref mut default) = self.default {
|
||||
Either::Right(default.call(req))
|
||||
} else {
|
||||
Either::Left(ok(req.error_response(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FilesService {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("FilesService")
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for FilesService {
|
||||
type Request = ServiceRequest;
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Future = FilesServiceFuture;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: ServiceRequest) -> Self::Future {
|
||||
let is_method_valid = if let Some(guard) = &self.guards {
|
||||
// execute user defined guards
|
||||
(**guard).check(req.head())
|
||||
} else {
|
||||
// default behavior
|
||||
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||
};
|
||||
|
||||
if !is_method_valid {
|
||||
return Either::Left(ok(req.into_response(
|
||||
actix_web::HttpResponse::MethodNotAllowed()
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.body("Request did not meet this resource's requirements."),
|
||||
)));
|
||||
}
|
||||
|
||||
let real_path: PathBufWrap = match req.match_info().path().parse() {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Either::Left(ok(req.error_response(e))),
|
||||
};
|
||||
|
||||
// full file path
|
||||
let path = match self.directory.join(&real_path).canonicalize() {
|
||||
Ok(path) => path,
|
||||
Err(e) => return self.handle_err(e, req),
|
||||
};
|
||||
|
||||
if path.is_dir() {
|
||||
if let Some(ref redir_index) = self.index {
|
||||
if self.redirect_to_slash && !req.path().ends_with('/') {
|
||||
let redirect_to = format!("{}/", req.path());
|
||||
|
||||
return Either::Left(ok(req.into_response(
|
||||
HttpResponse::Found()
|
||||
.header(header::LOCATION, redirect_to)
|
||||
.body("")
|
||||
.into_body(),
|
||||
)));
|
||||
}
|
||||
|
||||
let path = path.join(redir_index);
|
||||
|
||||
match NamedFile::open(path) {
|
||||
Ok(mut named_file) => {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition =
|
||||
mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
named_file.flags = self.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
Either::Left(ok(match named_file.into_response(&req) {
|
||||
Ok(item) => ServiceResponse::new(req, item),
|
||||
Err(e) => ServiceResponse::from_err(e, req),
|
||||
}))
|
||||
}
|
||||
Err(e) => self.handle_err(e, req),
|
||||
}
|
||||
} else if self.show_index {
|
||||
let dir = Directory::new(self.directory.clone(), path);
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let x = (self.renderer)(&dir, &req);
|
||||
|
||||
match x {
|
||||
Ok(resp) => Either::Left(ok(resp)),
|
||||
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
|
||||
}
|
||||
} else {
|
||||
Either::Left(ok(ServiceResponse::from_err(
|
||||
FilesError::IsDirectory,
|
||||
req.into_parts().0,
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
match NamedFile::open(path) {
|
||||
Ok(mut named_file) => {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition =
|
||||
mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
named_file.flags = self.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
match named_file.into_response(&req) {
|
||||
Ok(item) => {
|
||||
Either::Left(ok(ServiceResponse::new(req.clone(), item)))
|
||||
}
|
||||
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
|
||||
}
|
||||
}
|
||||
Err(e) => self.handle_err(e, req),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
actix-files/tests/encoding.rs
Normal file
40
actix-files/tests/encoding.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use actix_files::Files;
|
||||
use actix_web::{
|
||||
http::{
|
||||
header::{self, HeaderValue},
|
||||
StatusCode,
|
||||
},
|
||||
test::{self, TestRequest},
|
||||
App,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_utf8_file_contents() {
|
||||
// use default ISO-8859-1 encoding
|
||||
let mut srv =
|
||||
test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||
|
||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||
let res = test::call_service(&mut srv, req).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("text/plain")),
|
||||
);
|
||||
|
||||
// prefer UTF-8 encoding
|
||||
let mut srv = test::init_service(
|
||||
App::new().service(Files::new("/", "./tests").prefer_utf8(true)),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||
let res = test::call_service(&mut srv, req).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
|
||||
);
|
||||
}
|
3
actix-files/tests/utf8.txt
Normal file
3
actix-files/tests/utf8.txt
Normal file
@ -0,0 +1,3 @@
|
||||
中文内容显示正确。
|
||||
|
||||
English is OK.
|
@ -1,3 +0,0 @@
|
||||
# Framed app for actix web
|
||||
|
||||
**This crate has been deprecated and removed.**
|
@ -3,7 +3,40 @@
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 2.0.0-beta.2 - 2020-07-21
|
||||
## 2.1.0 - 2020-10-30
|
||||
### Added
|
||||
* Added more flexible `on_connect_ext` methods for on-connect handling. [#1754]
|
||||
|
||||
### Changed
|
||||
* Upgrade `base64` to `0.13`. [#1744]
|
||||
* Upgrade `pin-project` to `1.0`. [#1733]
|
||||
* Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760]
|
||||
|
||||
[#1760]: https://github.com/actix/actix-web/pull/1760
|
||||
[#1754]: https://github.com/actix/actix-web/pull/1754
|
||||
[#1733]: https://github.com/actix/actix-web/pull/1733
|
||||
[#1744]: https://github.com/actix/actix-web/pull/1744
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
* No significant changes from `2.0.0-beta.4`.
|
||||
|
||||
|
||||
## 2.0.0-beta.4 - 2020-09-09
|
||||
### Changed
|
||||
* Update actix-codec and actix-utils dependencies.
|
||||
* Update actix-connect and actix-tls dependencies.
|
||||
|
||||
|
||||
## [2.0.0-beta.3] - 2020-08-14
|
||||
|
||||
### Fixed
|
||||
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
|
||||
|
||||
[#1626]: https://github.com/actix/actix-web/pull/1626
|
||||
|
||||
|
||||
## [2.0.0-beta.2] - 2020-07-21
|
||||
### Fixed
|
||||
* Potential UB in h1 decoder using uninitialized memory. [#1614]
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "2.0.0-beta.2"
|
||||
version = "2.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix HTTP primitives"
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
@ -40,16 +40,16 @@ secure-cookies = ["cookie/secure"]
|
||||
actors = ["actix"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "1.0.5"
|
||||
actix-codec = "0.2.0"
|
||||
actix-connect = "2.0.0-alpha.3"
|
||||
actix-utils = "1.0.6"
|
||||
actix-service = "1.0.6"
|
||||
actix-codec = "0.3.0"
|
||||
actix-connect = "2.0.0"
|
||||
actix-utils = "2.0.0"
|
||||
actix-rt = "1.0.0"
|
||||
actix-threadpool = "0.3.1"
|
||||
actix-tls = { version = "2.0.0-alpha.1", optional = true }
|
||||
actix = { version = "0.10.0-alpha.1", optional = true }
|
||||
actix-tls = { version = "2.0.0", optional = true }
|
||||
actix = { version = "0.10.0", optional = true }
|
||||
|
||||
base64 = "0.12"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.2"
|
||||
bytes = "0.5.3"
|
||||
cookie = { version = "0.14.1", features = ["percent-encode"] }
|
||||
@ -71,7 +71,7 @@ language-tags = "0.2"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
pin-project = "0.4.17"
|
||||
pin-project = "1.0.0"
|
||||
rand = "0.7"
|
||||
regex = "1.3"
|
||||
serde = "1.0"
|
||||
@ -87,14 +87,14 @@ flate2 = { version = "1.0.13", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-server = "1.0.1"
|
||||
actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] }
|
||||
actix-tls = { version = "2.0.0-alpha.1", features = ["openssl"] }
|
||||
actix-connect = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-tls = { version = "2.0.0", features = ["openssl"] }
|
||||
criterion = "0.3"
|
||||
env_logger = "0.7"
|
||||
serde_derive = "1.0"
|
||||
open-ssl = { version="0.10", package = "openssl" }
|
||||
rust-tls = { version="0.17", package = "rustls" }
|
||||
rust-tls = { version="0.18", package = "rustls" }
|
||||
|
||||
[[bench]]
|
||||
name = "content-length"
|
||||
|
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-NOW Nikolay Kim
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
1
actix-http/LICENSE-APACHE
Symbolic link
1
actix-http/LICENSE-APACHE
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE-APACHE
|
@ -1,25 +0,0 @@
|
||||
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.
|
1
actix-http/LICENSE-MIT
Symbolic link
1
actix-http/LICENSE-MIT
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE-MIT
|
@ -1,24 +1,27 @@
|
||||
# Actix http [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-http) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# actix-http
|
||||
|
||||
Actix http
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
## Documentation & community resources
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/2.1.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-http/2.1.0)
|
||||
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-http/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-http](https://crates.io/crates/actix-http)
|
||||
* Minimum supported Rust version: 1.40 or later
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http/2.1.0)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum Supported Rust Version (MSRV): 1.42.0
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
// see examples/framed_hello.rs for complete list of used crates.
|
||||
use std::{env, io};
|
||||
|
||||
use actix_http::{HttpService, Response};
|
||||
use actix_server::Server;
|
||||
use futures::future;
|
||||
use futures_util::future;
|
||||
use http::header::HeaderValue;
|
||||
use log::info;
|
||||
|
||||
|
@ -714,7 +714,7 @@ mod tests {
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast");
|
||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||
body.push_str("!");
|
||||
body.push('!');
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast!");
|
||||
let not_body = resp_body.downcast_ref::<()>();
|
||||
|
@ -14,10 +14,11 @@ use crate::helpers::{Data, DataFactory};
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::service::HttpService;
|
||||
use crate::{ConnectCallback, Extensions};
|
||||
|
||||
/// A http service builder
|
||||
/// A HTTP service builder
|
||||
///
|
||||
/// This type can be used to construct an instance of `http service` through a
|
||||
/// This type can be used to construct an instance of [`HttpService`] through a
|
||||
/// builder-like pattern.
|
||||
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
|
||||
keep_alive: KeepAlive,
|
||||
@ -27,7 +28,9 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
// DEPRECATED: in favor of on_connect_ext
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_t: PhantomData<(T, S)>,
|
||||
}
|
||||
|
||||
@ -49,6 +52,7 @@ where
|
||||
expect: ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect: None,
|
||||
on_connect_ext: None,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -138,6 +142,7 @@ where
|
||||
expect: expect.into_factory(),
|
||||
upgrade: self.upgrade,
|
||||
on_connect: self.on_connect,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -167,14 +172,16 @@ where
|
||||
expect: self.expect,
|
||||
upgrade: Some(upgrade.into_factory()),
|
||||
on_connect: self.on_connect,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set on-connect callback.
|
||||
///
|
||||
/// It get called once per connection and result of the call
|
||||
/// get stored to the request's extensions.
|
||||
/// Called once per connection. Return value of the call is stored in request extensions.
|
||||
///
|
||||
/// *SOFT DEPRECATED*: Prefer the `on_connect_ext` style callback.
|
||||
pub fn on_connect<F, I>(mut self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&T) -> I + 'static,
|
||||
@ -184,7 +191,20 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Finish service configuration and create *http service* for HTTP/1 protocol.
|
||||
/// Sets the callback to be run on connection establishment.
|
||||
///
|
||||
/// Has mutable access to a data container that will be merged into request extensions.
|
||||
/// This enables transport layer data (like client certificates) to be accessed in middleware
|
||||
/// and handlers.
|
||||
pub fn on_connect_ext<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&T, &mut Extensions) + 'static,
|
||||
{
|
||||
self.on_connect_ext = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol.
|
||||
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
|
||||
where
|
||||
B: MessageBody,
|
||||
@ -200,13 +220,15 @@ where
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
|
||||
H1Service::with_config(cfg, service.into_factory())
|
||||
.expect(self.expect)
|
||||
.upgrade(self.upgrade)
|
||||
.on_connect(self.on_connect)
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create *http service* for HTTP/2 protocol.
|
||||
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
||||
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
@ -223,7 +245,10 @@ where
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
H2Service::with_config(cfg, service.into_factory()).on_connect(self.on_connect)
|
||||
|
||||
H2Service::with_config(cfg, service.into_factory())
|
||||
.on_connect(self.on_connect)
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create `HttpService` instance.
|
||||
@ -243,9 +268,11 @@ where
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
|
||||
HttpService::with_config(cfg, service.into_factory())
|
||||
.expect(self.expect)
|
||||
.upgrade(self.upgrade)
|
||||
.on_connect(self.on_connect)
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
}
|
||||
}
|
||||
|
@ -46,10 +46,10 @@ pub trait Connection {
|
||||
|
||||
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
|
||||
/// Close connection
|
||||
fn close(&mut self);
|
||||
fn close(self: Pin<&mut Self>);
|
||||
|
||||
/// Release connection to the connection pool
|
||||
fn release(&mut self);
|
||||
fn release(self: Pin<&mut Self>);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@ -195,11 +195,15 @@ where
|
||||
match self {
|
||||
EitherConnection::A(con) => con
|
||||
.open_tunnel(head)
|
||||
.map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::A))))
|
||||
.map(|res| {
|
||||
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A)))
|
||||
})
|
||||
.boxed_local(),
|
||||
EitherConnection::B(con) => con
|
||||
.open_tunnel(head)
|
||||
.map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::B))))
|
||||
.map(|res| {
|
||||
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B)))
|
||||
})
|
||||
.boxed_local(),
|
||||
}
|
||||
}
|
||||
|
@ -67,17 +67,17 @@ where
|
||||
};
|
||||
|
||||
// create Framed and send request
|
||||
let mut framed = Framed::new(io, h1::ClientCodec::default());
|
||||
framed.send((head, body.size()).into()).await?;
|
||||
let mut framed_inner = Framed::new(io, h1::ClientCodec::default());
|
||||
framed_inner.send((head, body.size()).into()).await?;
|
||||
|
||||
// send request body
|
||||
match body.size() {
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0) => (),
|
||||
_ => send_body(body, &mut framed).await?,
|
||||
_ => send_body(body, Pin::new(&mut framed_inner)).await?,
|
||||
};
|
||||
|
||||
// read response and init read body
|
||||
let res = framed.into_future().await;
|
||||
let res = Pin::new(&mut framed_inner).into_future().await;
|
||||
let (head, framed) = if let (Some(result), framed) = res {
|
||||
let item = result.map_err(SendRequestError::from)?;
|
||||
(item, framed)
|
||||
@ -85,14 +85,14 @@ where
|
||||
return Err(SendRequestError::from(ConnectError::Disconnected));
|
||||
};
|
||||
|
||||
match framed.get_codec().message_type() {
|
||||
match framed.codec_ref().message_type() {
|
||||
h1::MessageType::None => {
|
||||
let force_close = !framed.get_codec().keepalive();
|
||||
let force_close = !framed.codec_ref().keepalive();
|
||||
release_connection(framed, force_close);
|
||||
Ok((head, Payload::None))
|
||||
}
|
||||
_ => {
|
||||
let pl: PayloadStream = PlStream::new(framed).boxed_local();
|
||||
let pl: PayloadStream = PlStream::new(framed_inner).boxed_local();
|
||||
Ok((head, pl.into()))
|
||||
}
|
||||
}
|
||||
@ -119,35 +119,36 @@ where
|
||||
}
|
||||
|
||||
/// send request body to the peer
|
||||
pub(crate) async fn send_body<I, B>(
|
||||
pub(crate) async fn send_body<T, B>(
|
||||
body: B,
|
||||
framed: &mut Framed<I, h1::ClientCodec>,
|
||||
mut framed: Pin<&mut Framed<T, h1::ClientCodec>>,
|
||||
) -> Result<(), SendRequestError>
|
||||
where
|
||||
I: ConnectionLifetime,
|
||||
T: ConnectionLifetime + Unpin,
|
||||
B: MessageBody,
|
||||
{
|
||||
let mut eof = false;
|
||||
pin_mut!(body);
|
||||
|
||||
let mut eof = false;
|
||||
while !eof {
|
||||
while !eof && !framed.is_write_buf_full() {
|
||||
while !eof && !framed.as_ref().is_write_buf_full() {
|
||||
match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
||||
Some(result) => {
|
||||
framed.write(h1::Message::Chunk(Some(result?)))?;
|
||||
framed.as_mut().write(h1::Message::Chunk(Some(result?)))?;
|
||||
}
|
||||
None => {
|
||||
eof = true;
|
||||
framed.write(h1::Message::Chunk(None))?;
|
||||
framed.as_mut().write(h1::Message::Chunk(None))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !framed.is_write_buf_empty() {
|
||||
poll_fn(|cx| match framed.flush(cx) {
|
||||
if !framed.as_ref().is_write_buf_empty() {
|
||||
poll_fn(|cx| match framed.as_mut().flush(cx) {
|
||||
Poll::Ready(Ok(_)) => Poll::Ready(Ok(())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => {
|
||||
if !framed.is_write_buf_full() {
|
||||
if !framed.as_ref().is_write_buf_full() {
|
||||
Poll::Ready(Ok(()))
|
||||
} else {
|
||||
Poll::Pending
|
||||
@ -158,13 +159,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
SinkExt::flush(framed).await?;
|
||||
SinkExt::flush(Pin::into_inner(framed)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// HTTP client connection
|
||||
pub struct H1Connection<T> {
|
||||
/// T should be `Unpin`
|
||||
io: Option<T>,
|
||||
created: time::Instant,
|
||||
pool: Option<Acquired<T>>,
|
||||
@ -175,7 +177,7 @@ where
|
||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
/// Close connection
|
||||
fn close(&mut self) {
|
||||
fn close(mut self: Pin<&mut Self>) {
|
||||
if let Some(mut pool) = self.pool.take() {
|
||||
if let Some(io) = self.io.take() {
|
||||
pool.close(IoConnection::new(
|
||||
@ -188,7 +190,7 @@ where
|
||||
}
|
||||
|
||||
/// Release this connection to the connection pool
|
||||
fn release(&mut self) {
|
||||
fn release(mut self: Pin<&mut Self>) {
|
||||
if let Some(mut pool) = self.pool.take() {
|
||||
if let Some(io) = self.io.take() {
|
||||
pool.release(IoConnection::new(
|
||||
@ -242,14 +244,18 @@ impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncWrite for H1Connection<T>
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pub(crate) struct PlStream<Io> {
|
||||
#[pin]
|
||||
framed: Option<Framed<Io, h1::ClientPayloadCodec>>,
|
||||
}
|
||||
|
||||
impl<Io: ConnectionLifetime> PlStream<Io> {
|
||||
fn new(framed: Framed<Io, h1::ClientCodec>) -> Self {
|
||||
let framed = framed.into_map_codec(|codec| codec.into_payload_codec());
|
||||
|
||||
PlStream {
|
||||
framed: Some(framed.map_codec(|codec| codec.into_payload_codec())),
|
||||
framed: Some(framed),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -261,16 +267,16 @@ impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
let mut this = self.project();
|
||||
|
||||
match this.framed.as_mut().unwrap().next_item(cx)? {
|
||||
match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Some(chunk)) => {
|
||||
if let Some(chunk) = chunk {
|
||||
Poll::Ready(Some(Ok(chunk)))
|
||||
} else {
|
||||
let framed = this.framed.take().unwrap();
|
||||
let force_close = !framed.get_codec().keepalive();
|
||||
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
||||
let force_close = !framed.codec_ref().keepalive();
|
||||
release_connection(framed, force_close);
|
||||
Poll::Ready(None)
|
||||
}
|
||||
@ -280,14 +286,13 @@ impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
|
||||
}
|
||||
}
|
||||
|
||||
fn release_connection<T, U>(framed: Framed<T, U>, force_close: bool)
|
||||
fn release_connection<T, U>(framed: Pin<&mut Framed<T, U>>, force_close: bool)
|
||||
where
|
||||
T: ConnectionLifetime,
|
||||
{
|
||||
let mut parts = framed.into_parts();
|
||||
if !force_close && parts.read_buf.is_empty() && parts.write_buf.is_empty() {
|
||||
parts.io.release()
|
||||
if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() {
|
||||
framed.io_pin().release()
|
||||
} else {
|
||||
parts.io.close()
|
||||
framed.io_pin().close()
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@ -65,8 +65,8 @@ where
|
||||
|
||||
// start support future
|
||||
actix_rt::spawn(ConnectorPoolSupport {
|
||||
connector: connector_rc.clone(),
|
||||
inner: Rc::downgrade(&inner_rc),
|
||||
connector: Rc::clone(&connector_rc),
|
||||
inner: Rc::clone(&inner_rc),
|
||||
});
|
||||
|
||||
ConnectionPool(connector_rc, inner_rc)
|
||||
@ -82,6 +82,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Io> Drop for ConnectionPool<T, Io> {
|
||||
fn drop(&mut self) {
|
||||
// wake up the ConnectorPoolSupport when dropping so it can exit properly.
|
||||
self.1.borrow().waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Io> Service for ConnectionPool<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
@ -112,11 +119,11 @@ where
|
||||
match poll_fn(|cx| Poll::Ready(inner.borrow_mut().acquire(&key, cx))).await {
|
||||
Acquire::Acquired(io, created) => {
|
||||
// use existing connection
|
||||
return Ok(IoConnection::new(
|
||||
Ok(IoConnection::new(
|
||||
io,
|
||||
created,
|
||||
Some(Acquired(key, Some(inner))),
|
||||
));
|
||||
))
|
||||
}
|
||||
Acquire::Available => {
|
||||
// open tcp connection
|
||||
@ -421,7 +428,7 @@ where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
connector: T,
|
||||
inner: Weak<RefCell<Inner<Io>>>,
|
||||
inner: Rc<RefCell<Inner<Io>>>,
|
||||
}
|
||||
|
||||
impl<T, Io> Future for ConnectorPoolSupport<T, Io>
|
||||
@ -435,55 +442,57 @@ where
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
|
||||
if let Some(this_inner) = this.inner.upgrade() {
|
||||
let mut inner = this_inner.as_ref().borrow_mut();
|
||||
inner.waker.register(cx.waker());
|
||||
if Rc::strong_count(this.inner) == 1 {
|
||||
// If we are last copy of Inner<Io> it means the ConnectionPool is already gone
|
||||
// and we are safe to exit.
|
||||
return Poll::Ready(());
|
||||
}
|
||||
|
||||
// check waiters
|
||||
loop {
|
||||
let (key, token) = {
|
||||
if let Some((key, token)) = inner.waiters_queue.get_index(0) {
|
||||
(key.clone(), *token)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
};
|
||||
if inner.waiters.get(token).unwrap().is_none() {
|
||||
continue;
|
||||
}
|
||||
let mut inner = this.inner.borrow_mut();
|
||||
inner.waker.register(cx.waker());
|
||||
|
||||
match inner.acquire(&key, cx) {
|
||||
Acquire::NotAvailable => break,
|
||||
Acquire::Acquired(io, created) => {
|
||||
let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1;
|
||||
if let Err(conn) = tx.send(Ok(IoConnection::new(
|
||||
io,
|
||||
created,
|
||||
Some(Acquired(key.clone(), Some(this_inner.clone()))),
|
||||
))) {
|
||||
let (io, created) = conn.unwrap().into_inner();
|
||||
inner.release_conn(&key, io, created);
|
||||
}
|
||||
}
|
||||
Acquire::Available => {
|
||||
let (connect, tx) =
|
||||
inner.waiters.get_mut(token).unwrap().take().unwrap();
|
||||
OpenWaitingConnection::spawn(
|
||||
key.clone(),
|
||||
tx,
|
||||
this_inner.clone(),
|
||||
this.connector.call(connect),
|
||||
inner.config.clone(),
|
||||
);
|
||||
}
|
||||
// check waiters
|
||||
loop {
|
||||
let (key, token) = {
|
||||
if let Some((key, token)) = inner.waiters_queue.get_index(0) {
|
||||
(key.clone(), *token)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
let _ = inner.waiters_queue.swap_remove_index(0);
|
||||
};
|
||||
if inner.waiters.get(token).unwrap().is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
} else {
|
||||
Poll::Ready(())
|
||||
match inner.acquire(&key, cx) {
|
||||
Acquire::NotAvailable => break,
|
||||
Acquire::Acquired(io, created) => {
|
||||
let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1;
|
||||
if let Err(conn) = tx.send(Ok(IoConnection::new(
|
||||
io,
|
||||
created,
|
||||
Some(Acquired(key.clone(), Some(this.inner.clone()))),
|
||||
))) {
|
||||
let (io, created) = conn.unwrap().into_inner();
|
||||
inner.release_conn(&key, io, created);
|
||||
}
|
||||
}
|
||||
Acquire::Available => {
|
||||
let (connect, tx) =
|
||||
inner.waiters.get_mut(token).unwrap().take().unwrap();
|
||||
OpenWaitingConnection::spawn(
|
||||
key.clone(),
|
||||
tx,
|
||||
this.inner.clone(),
|
||||
this.connector.call(connect),
|
||||
inner.config.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let _ = inner.waiters_queue.swap_remove_index(0);
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ const DATE_VALUE_LENGTH: usize = 29;
|
||||
pub enum KeepAlive {
|
||||
/// Keep alive in seconds
|
||||
Timeout(usize),
|
||||
/// Relay on OS to shutdown tcp connection
|
||||
/// Rely on OS to shutdown tcp connection
|
||||
Os,
|
||||
/// Disabled
|
||||
Disabled,
|
||||
@ -209,6 +209,7 @@ impl Date {
|
||||
date.update();
|
||||
date
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.pos = 0;
|
||||
write!(
|
||||
|
@ -1,4 +1,5 @@
|
||||
//! Error and Result module
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::io::Write;
|
||||
use std::str::Utf8Error;
|
||||
@ -7,7 +8,7 @@ use std::{fmt, io, result};
|
||||
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
pub use actix_threadpool::BlockingError;
|
||||
use actix_utils::framed::DispatcherError as FramedDispatcherError;
|
||||
use actix_utils::dispatcher::DispatcherError as FramedDispatcherError;
|
||||
use actix_utils::timeout::TimeoutError;
|
||||
use bytes::BytesMut;
|
||||
use derive_more::{Display, From};
|
||||
@ -452,10 +453,10 @@ impl ResponseError for ContentTypeError {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, U: Encoder + Decoder> ResponseError for FramedDispatcherError<E, U>
|
||||
impl<E, U: Encoder<I> + Decoder, I> ResponseError for FramedDispatcherError<E, U, I>
|
||||
where
|
||||
E: fmt::Debug + fmt::Display,
|
||||
<U as Encoder>::Error: fmt::Debug,
|
||||
<U as Encoder<I>>::Error: fmt::Debug,
|
||||
<U as Decoder>::Error: fmt::Debug,
|
||||
{
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::fmt;
|
||||
use std::{fmt, mem};
|
||||
|
||||
use fxhash::FxHashMap;
|
||||
|
||||
@ -61,6 +61,16 @@ impl Extensions {
|
||||
pub fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
|
||||
/// Extends self with the items from another `Extensions`.
|
||||
pub fn extend(&mut self, other: Extensions) {
|
||||
self.map.extend(other.map);
|
||||
}
|
||||
|
||||
/// Sets (or overrides) items from `other` into this map.
|
||||
pub(crate) fn drain_from(&mut self, other: &mut Self) {
|
||||
self.map.extend(mem::take(&mut other.map));
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Extensions {
|
||||
@ -178,4 +188,57 @@ mod tests {
|
||||
assert_eq!(extensions.get::<bool>(), None);
|
||||
assert_eq!(extensions.get(), Some(&MyType(10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend() {
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct MyType(i32);
|
||||
|
||||
let mut extensions = Extensions::new();
|
||||
|
||||
extensions.insert(5i32);
|
||||
extensions.insert(MyType(10));
|
||||
|
||||
let mut other = Extensions::new();
|
||||
|
||||
other.insert(15i32);
|
||||
other.insert(20u8);
|
||||
|
||||
extensions.extend(other);
|
||||
|
||||
assert_eq!(extensions.get(), Some(&15i32));
|
||||
assert_eq!(extensions.get_mut(), Some(&mut 15i32));
|
||||
|
||||
assert_eq!(extensions.remove::<i32>(), Some(15i32));
|
||||
assert!(extensions.get::<i32>().is_none());
|
||||
|
||||
assert_eq!(extensions.get::<bool>(), None);
|
||||
assert_eq!(extensions.get(), Some(&MyType(10)));
|
||||
|
||||
assert_eq!(extensions.get(), Some(&20u8));
|
||||
assert_eq!(extensions.get_mut(), Some(&mut 20u8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drain_from() {
|
||||
let mut ext = Extensions::new();
|
||||
ext.insert(2isize);
|
||||
|
||||
let mut more_ext = Extensions::new();
|
||||
|
||||
more_ext.insert(5isize);
|
||||
more_ext.insert(5usize);
|
||||
|
||||
assert_eq!(ext.get::<isize>(), Some(&2isize));
|
||||
assert_eq!(ext.get::<usize>(), None);
|
||||
assert_eq!(more_ext.get::<isize>(), Some(&5isize));
|
||||
assert_eq!(more_ext.get::<usize>(), Some(&5usize));
|
||||
|
||||
ext.drain_from(&mut more_ext);
|
||||
|
||||
assert_eq!(ext.get::<isize>(), Some(&5isize));
|
||||
assert_eq!(ext.get::<usize>(), Some(&5usize));
|
||||
assert_eq!(more_ext.get::<isize>(), None);
|
||||
assert_eq!(more_ext.get::<usize>(), None);
|
||||
}
|
||||
}
|
||||
|
@ -173,13 +173,12 @@ impl Decoder for ClientPayloadCodec {
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for ClientCodec {
|
||||
type Item = Message<(RequestHeadType, BodySize)>;
|
||||
impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(
|
||||
&mut self,
|
||||
item: Self::Item,
|
||||
item: Message<(RequestHeadType, BodySize)>,
|
||||
dst: &mut BytesMut,
|
||||
) -> Result<(), Self::Error> {
|
||||
match item {
|
||||
|
@ -144,13 +144,12 @@ impl Decoder for Codec {
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for Codec {
|
||||
type Item = Message<(Response<()>, BodySize)>;
|
||||
impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(
|
||||
&mut self,
|
||||
item: Self::Item,
|
||||
item: Message<(Response<()>, BodySize)>,
|
||||
dst: &mut BytesMut,
|
||||
) -> Result<(), Self::Error> {
|
||||
match item {
|
||||
|
@ -76,12 +76,14 @@ pub(crate) trait MessageType: Sized {
|
||||
let name =
|
||||
HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap();
|
||||
|
||||
// SAFETY: httparse checks header value is valid UTF-8
|
||||
// SAFETY: httparse already checks header value is only visible ASCII bytes
|
||||
// from_maybe_shared_unchecked contains debug assertions so they are omitted here
|
||||
let value = unsafe {
|
||||
HeaderValue::from_maybe_shared_unchecked(
|
||||
slice.slice(idx.value.0..idx.value.1),
|
||||
)
|
||||
};
|
||||
|
||||
match name {
|
||||
header::CONTENT_LENGTH => {
|
||||
if let Ok(s) = value.to_str() {
|
||||
|
@ -12,7 +12,6 @@ use bytes::{Buf, BytesMut};
|
||||
use log::{error, trace};
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::body::{Body, BodySize, MessageBody, ResponseBody};
|
||||
use crate::cloneable::CloneableService;
|
||||
use crate::config::ServiceConfig;
|
||||
use crate::error::{DispatchError, Error};
|
||||
@ -21,6 +20,10 @@ use crate::helpers::DataFactory;
|
||||
use crate::httpmessage::HttpMessage;
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::{
|
||||
body::{Body, BodySize, MessageBody, ResponseBody},
|
||||
Extensions,
|
||||
};
|
||||
|
||||
use super::codec::Codec;
|
||||
use super::payload::{Payload, PayloadSender, PayloadStatus};
|
||||
@ -88,6 +91,7 @@ where
|
||||
expect: CloneableService<X>,
|
||||
upgrade: Option<CloneableService<U>>,
|
||||
on_connect: Option<Box<dyn DataFactory>>,
|
||||
on_connect_data: Extensions,
|
||||
flags: Flags,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
error: Option<DispatchError>,
|
||||
@ -167,7 +171,7 @@ where
|
||||
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
/// Create http/1 dispatcher.
|
||||
/// Create HTTP/1 dispatcher.
|
||||
pub(crate) fn new(
|
||||
stream: T,
|
||||
config: ServiceConfig,
|
||||
@ -175,6 +179,7 @@ where
|
||||
expect: CloneableService<X>,
|
||||
upgrade: Option<CloneableService<U>>,
|
||||
on_connect: Option<Box<dyn DataFactory>>,
|
||||
on_connect_data: Extensions,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
) -> Self {
|
||||
Dispatcher::with_timeout(
|
||||
@ -187,6 +192,7 @@ where
|
||||
expect,
|
||||
upgrade,
|
||||
on_connect,
|
||||
on_connect_data,
|
||||
peer_addr,
|
||||
)
|
||||
}
|
||||
@ -202,6 +208,7 @@ where
|
||||
expect: CloneableService<X>,
|
||||
upgrade: Option<CloneableService<U>>,
|
||||
on_connect: Option<Box<dyn DataFactory>>,
|
||||
on_connect_data: Extensions,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
) -> Self {
|
||||
let keepalive = config.keep_alive_enabled();
|
||||
@ -234,6 +241,7 @@ where
|
||||
expect,
|
||||
upgrade,
|
||||
on_connect,
|
||||
on_connect_data,
|
||||
flags,
|
||||
peer_addr,
|
||||
ka_expire,
|
||||
@ -314,11 +322,15 @@ where
|
||||
Poll::Ready(Err(err)) => return Err(DispatchError::Io(err)),
|
||||
}
|
||||
}
|
||||
|
||||
if written == write_buf.len() {
|
||||
// SAFETY: setting length to 0 is safe
|
||||
// skips one length check vs truncate
|
||||
unsafe { write_buf.set_len(0) }
|
||||
} else {
|
||||
write_buf.advance(written);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@ -522,11 +534,15 @@ where
|
||||
let pl = this.codec.message_type();
|
||||
req.head_mut().peer_addr = *this.peer_addr;
|
||||
|
||||
// DEPRECATED
|
||||
// set on_connect data
|
||||
if let Some(ref on_connect) = this.on_connect {
|
||||
on_connect.set(&mut req.extensions_mut());
|
||||
}
|
||||
|
||||
// merge on_connect_ext data into request extensions
|
||||
req.extensions_mut().drain_from(this.on_connect_data);
|
||||
|
||||
if pl == MessageType::Stream && this.upgrade.is_some() {
|
||||
this.messages.push_back(DispatcherMessage::Upgrade(req));
|
||||
break;
|
||||
@ -923,8 +939,10 @@ mod tests {
|
||||
CloneableService::new(ExpectHandler),
|
||||
None,
|
||||
None,
|
||||
Extensions::new(),
|
||||
None,
|
||||
);
|
||||
|
||||
match Pin::new(&mut h1).poll(cx) {
|
||||
Poll::Pending => panic!(),
|
||||
Poll::Ready(res) => assert!(res.is_err()),
|
||||
|
@ -129,89 +129,133 @@ pub(crate) trait MessageType: Sized {
|
||||
.chain(extra_headers.inner.iter());
|
||||
|
||||
// write headers
|
||||
let mut pos = 0;
|
||||
|
||||
let mut has_date = false;
|
||||
let mut remaining = dst.capacity() - dst.len();
|
||||
|
||||
let mut buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
|
||||
let mut remaining = dst.capacity() - dst.len();
|
||||
|
||||
// tracks bytes written since last buffer resize
|
||||
// since buf is a raw pointer to a bytes container storage but is written to without the
|
||||
// container's knowledge, this is used to sync the containers cursor after data is written
|
||||
let mut pos = 0;
|
||||
|
||||
for (key, value) in headers {
|
||||
match *key {
|
||||
CONNECTION => continue,
|
||||
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue,
|
||||
DATE => {
|
||||
has_date = true;
|
||||
}
|
||||
DATE => has_date = true,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let k = key.as_str().as_bytes();
|
||||
let k_len = k.len();
|
||||
|
||||
match value {
|
||||
map::Value::One(ref val) => {
|
||||
let v = val.as_ref();
|
||||
let v_len = v.len();
|
||||
let k_len = k.len();
|
||||
|
||||
// key length + value length + colon + space + \r\n
|
||||
let len = k_len + v_len + 4;
|
||||
|
||||
if len > remaining {
|
||||
// not enough room in buffer for this header; reserve more space
|
||||
|
||||
// SAFETY: all the bytes written up to position "pos" are initialized
|
||||
// the written byte count and pointer advancement are kept in sync
|
||||
unsafe {
|
||||
dst.advance_mut(pos);
|
||||
}
|
||||
|
||||
pos = 0;
|
||||
dst.reserve(len * 2);
|
||||
remaining = dst.capacity() - dst.len();
|
||||
|
||||
// re-assign buf raw pointer since it's possible that the buffer was
|
||||
// reallocated and/or resized
|
||||
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
|
||||
}
|
||||
// use upper Camel-Case
|
||||
|
||||
// SAFETY: on each write, it is enough to ensure that the advancement of the
|
||||
// cursor matches the number of bytes written
|
||||
unsafe {
|
||||
// use upper Camel-Case
|
||||
if camel_case {
|
||||
write_camel_case(k, from_raw_parts_mut(buf, k_len))
|
||||
} else {
|
||||
write_data(k, buf, k_len)
|
||||
}
|
||||
|
||||
buf = buf.add(k_len);
|
||||
|
||||
write_data(b": ", buf, 2);
|
||||
buf = buf.add(2);
|
||||
|
||||
write_data(v, buf, v_len);
|
||||
buf = buf.add(v_len);
|
||||
|
||||
write_data(b"\r\n", buf, 2);
|
||||
buf = buf.add(2);
|
||||
pos += len;
|
||||
remaining -= len;
|
||||
}
|
||||
|
||||
pos += len;
|
||||
remaining -= len;
|
||||
}
|
||||
|
||||
map::Value::Multi(ref vec) => {
|
||||
for val in vec {
|
||||
let v = val.as_ref();
|
||||
let v_len = v.len();
|
||||
let k_len = k.len();
|
||||
let len = k_len + v_len + 4;
|
||||
|
||||
if len > remaining {
|
||||
// SAFETY: all the bytes written up to position "pos" are initialized
|
||||
// the written byte count and pointer advancement are kept in sync
|
||||
unsafe {
|
||||
dst.advance_mut(pos);
|
||||
}
|
||||
pos = 0;
|
||||
dst.reserve(len * 2);
|
||||
remaining = dst.capacity() - dst.len();
|
||||
|
||||
// re-assign buf raw pointer since it's possible that the buffer was
|
||||
// reallocated and/or resized
|
||||
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
|
||||
}
|
||||
// use upper Camel-Case
|
||||
|
||||
// SAFETY: on each write, it is enough to ensure that the advancement of
|
||||
// the cursor matches the number of bytes written
|
||||
unsafe {
|
||||
if camel_case {
|
||||
write_camel_case(k, from_raw_parts_mut(buf, k_len));
|
||||
} else {
|
||||
write_data(k, buf, k_len);
|
||||
}
|
||||
|
||||
buf = buf.add(k_len);
|
||||
|
||||
write_data(b": ", buf, 2);
|
||||
buf = buf.add(2);
|
||||
|
||||
write_data(v, buf, v_len);
|
||||
buf = buf.add(v_len);
|
||||
|
||||
write_data(b"\r\n", buf, 2);
|
||||
buf = buf.add(2);
|
||||
};
|
||||
|
||||
pos += len;
|
||||
remaining -= len;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// final cursor synchronization with the bytes container
|
||||
//
|
||||
// SAFETY: all the bytes written up to position "pos" are initialized
|
||||
// the written byte count and pointer advancement are kept in sync
|
||||
unsafe {
|
||||
dst.advance_mut(pos);
|
||||
}
|
||||
@ -477,7 +521,10 @@ impl<'a> io::Write for Writer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Callers must ensure that the given length matches given value length.
|
||||
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
|
||||
debug_assert_eq!(value.len(), len);
|
||||
copy_nonoverlapping(value.as_ptr(), buf, len);
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ use crate::error::{DispatchError, Error, ParseError};
|
||||
use crate::helpers::DataFactory;
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::{ConnectCallback, Extensions};
|
||||
|
||||
use super::codec::Codec;
|
||||
use super::dispatcher::Dispatcher;
|
||||
@ -30,6 +31,7 @@ pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler<T>> {
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
}
|
||||
|
||||
@ -52,6 +54,7 @@ where
|
||||
expect: ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect: None,
|
||||
on_connect_ext: None,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -98,7 +101,7 @@ mod openssl {
|
||||
use super::*;
|
||||
|
||||
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
|
||||
use actix_tls::{openssl::HandshakeError, SslError};
|
||||
use actix_tls::{openssl::HandshakeError, TlsError};
|
||||
|
||||
impl<S, B, X, U> H1Service<SslStream<TcpStream>, S, B, X, U>
|
||||
where
|
||||
@ -126,19 +129,19 @@ mod openssl {
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<HandshakeError<TcpStream>, DispatchError>,
|
||||
Error = TlsError<HandshakeError<TcpStream>, DispatchError>,
|
||||
InitError = (),
|
||||
> {
|
||||
pipeline_factory(
|
||||
Acceptor::new(acceptor)
|
||||
.map_err(SslError::Ssl)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(|io: SslStream<TcpStream>| {
|
||||
let peer_addr = io.get_ref().peer_addr().ok();
|
||||
ok((io, peer_addr))
|
||||
})
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -147,7 +150,7 @@ mod openssl {
|
||||
mod rustls {
|
||||
use super::*;
|
||||
use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
|
||||
use actix_tls::SslError;
|
||||
use actix_tls::TlsError;
|
||||
use std::{fmt, io};
|
||||
|
||||
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
|
||||
@ -176,19 +179,19 @@ mod rustls {
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<io::Error, DispatchError>,
|
||||
Error = TlsError<io::Error, DispatchError>,
|
||||
InitError = (),
|
||||
> {
|
||||
pipeline_factory(
|
||||
Acceptor::new(config)
|
||||
.map_err(SslError::Ssl)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(|io: TlsStream<TcpStream>| {
|
||||
let peer_addr = io.get_ref().0.peer_addr().ok();
|
||||
ok((io, peer_addr))
|
||||
})
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,6 +216,7 @@ where
|
||||
srv: self.srv,
|
||||
upgrade: self.upgrade,
|
||||
on_connect: self.on_connect,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -229,6 +233,7 @@ where
|
||||
srv: self.srv,
|
||||
expect: self.expect,
|
||||
on_connect: self.on_connect,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -241,6 +246,12 @@ where
|
||||
self.on_connect = f;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set on connect callback.
|
||||
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
|
||||
self.on_connect_ext = f;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> ServiceFactory for H1Service<T, S, B, X, U>
|
||||
@ -274,6 +285,7 @@ where
|
||||
expect: None,
|
||||
upgrade: None,
|
||||
on_connect: self.on_connect.clone(),
|
||||
on_connect_ext: self.on_connect_ext.clone(),
|
||||
cfg: Some(self.cfg.clone()),
|
||||
_t: PhantomData,
|
||||
}
|
||||
@ -303,6 +315,7 @@ where
|
||||
expect: Option<X::Service>,
|
||||
upgrade: Option<U::Service>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
cfg: Option<ServiceConfig>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
}
|
||||
@ -352,23 +365,26 @@ where
|
||||
|
||||
Poll::Ready(result.map(|service| {
|
||||
let this = self.as_mut().project();
|
||||
|
||||
H1ServiceHandler::new(
|
||||
this.cfg.take().unwrap(),
|
||||
service,
|
||||
this.expect.take().unwrap(),
|
||||
this.upgrade.take(),
|
||||
this.on_connect.clone(),
|
||||
this.on_connect_ext.clone(),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// `Service` implementation for HTTP1 transport
|
||||
/// `Service` implementation for HTTP/1 transport
|
||||
pub struct H1ServiceHandler<T, S: Service, B, X: Service, U: Service> {
|
||||
srv: CloneableService<S>,
|
||||
expect: CloneableService<X>,
|
||||
upgrade: Option<CloneableService<U>>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
cfg: ServiceConfig,
|
||||
_t: PhantomData<(T, B)>,
|
||||
}
|
||||
@ -390,6 +406,7 @@ where
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
) -> H1ServiceHandler<T, S, B, X, U> {
|
||||
H1ServiceHandler {
|
||||
srv: CloneableService::new(srv),
|
||||
@ -397,6 +414,7 @@ where
|
||||
upgrade: upgrade.map(CloneableService::new),
|
||||
cfg,
|
||||
on_connect,
|
||||
on_connect_ext,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -462,11 +480,13 @@ where
|
||||
}
|
||||
|
||||
fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
|
||||
let on_connect = if let Some(ref on_connect) = self.on_connect {
|
||||
Some(on_connect(&io))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
|
||||
|
||||
let mut connect_extensions = Extensions::new();
|
||||
if let Some(ref handler) = self.on_connect_ext {
|
||||
// run on_connect_ext callback, populating connect extensions
|
||||
handler(&io, &mut connect_extensions);
|
||||
}
|
||||
|
||||
Dispatcher::new(
|
||||
io,
|
||||
@ -474,7 +494,8 @@ where
|
||||
self.srv.clone(),
|
||||
self.expect.clone(),
|
||||
self.upgrade.clone(),
|
||||
on_connect,
|
||||
deprecated_on_connect,
|
||||
connect_extensions,
|
||||
addr,
|
||||
)
|
||||
}
|
||||
@ -548,10 +569,12 @@ where
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[pin_project::pin_project]
|
||||
pub struct OneRequestServiceResponse<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
#[pin]
|
||||
framed: Option<Framed<T, Codec>>,
|
||||
}
|
||||
|
||||
@ -562,16 +585,18 @@ where
|
||||
type Output = Result<(Request, Framed<T, Codec>), ParseError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.framed.as_mut().unwrap().next_item(cx) {
|
||||
Poll::Ready(Some(Ok(req))) => match req {
|
||||
let this = self.as_mut().project();
|
||||
|
||||
match ready!(this.framed.as_pin_mut().unwrap().next_item(cx)) {
|
||||
Some(Ok(req)) => match req {
|
||||
Message::Item(req) => {
|
||||
Poll::Ready(Ok((req, self.framed.take().unwrap())))
|
||||
let mut this = self.as_mut().project();
|
||||
Poll::Ready(Ok((req, this.framed.take().unwrap())))
|
||||
}
|
||||
Message::Chunk(_) => unreachable!("Something is wrong"),
|
||||
},
|
||||
Poll::Ready(Some(Err(err))) => Poll::Ready(Err(err)),
|
||||
Poll::Ready(None) => Poll::Ready(Err(ParseError::Incomplete)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
Some(Err(err)) => Poll::Ready(Err(err)),
|
||||
None => Poll::Ready(Err(ParseError::Incomplete)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,13 @@ use crate::error::Error;
|
||||
use crate::h1::{Codec, Message};
|
||||
use crate::response::Response;
|
||||
|
||||
/// Send http/1 response
|
||||
/// Send HTTP/1 response
|
||||
#[pin_project::pin_project]
|
||||
pub struct SendResponse<T, B> {
|
||||
res: Option<Message<(Response<()>, BodySize)>>,
|
||||
#[pin]
|
||||
body: Option<ResponseBody<B>>,
|
||||
#[pin]
|
||||
framed: Option<Framed<T, Codec>>,
|
||||
}
|
||||
|
||||
@ -35,23 +36,30 @@ where
|
||||
|
||||
impl<T, B> Future for SendResponse<T, B>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
B: MessageBody + Unpin,
|
||||
{
|
||||
type Output = Result<Framed<T, Codec>, Error>;
|
||||
|
||||
// TODO: rethink if we need loops in polls
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let mut this = self.project();
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let mut this = self.as_mut().project();
|
||||
|
||||
let mut body_done = this.body.is_none();
|
||||
loop {
|
||||
let mut body_ready = !body_done;
|
||||
let framed = this.framed.as_mut().unwrap();
|
||||
|
||||
// send body
|
||||
if this.res.is_none() && body_ready {
|
||||
while body_ready && !body_done && !framed.is_write_buf_full() {
|
||||
while body_ready
|
||||
&& !body_done
|
||||
&& !this
|
||||
.framed
|
||||
.as_ref()
|
||||
.as_pin_ref()
|
||||
.unwrap()
|
||||
.is_write_buf_full()
|
||||
{
|
||||
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? {
|
||||
Poll::Ready(item) => {
|
||||
// body is done when item is None
|
||||
@ -59,6 +67,7 @@ where
|
||||
if body_done {
|
||||
let _ = this.body.take();
|
||||
}
|
||||
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
||||
framed.write(Message::Chunk(item))?;
|
||||
}
|
||||
Poll::Pending => body_ready = false,
|
||||
@ -66,6 +75,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
||||
|
||||
// flush write buffer
|
||||
if !framed.is_write_buf_empty() {
|
||||
match framed.flush(cx)? {
|
||||
@ -96,6 +107,9 @@ where
|
||||
break;
|
||||
}
|
||||
}
|
||||
Poll::Ready(Ok(this.framed.take().unwrap()))
|
||||
|
||||
let framed = this.framed.take().unwrap();
|
||||
|
||||
Poll::Ready(Ok(framed))
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ use crate::message::ResponseHead;
|
||||
use crate::payload::Payload;
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::Extensions;
|
||||
|
||||
const CHUNK_SIZE: usize = 16_384;
|
||||
|
||||
@ -36,6 +37,7 @@ where
|
||||
service: CloneableService<S>,
|
||||
connection: Connection<T, Bytes>,
|
||||
on_connect: Option<Box<dyn DataFactory>>,
|
||||
on_connect_data: Extensions,
|
||||
config: ServiceConfig,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
ka_expire: Instant,
|
||||
@ -56,6 +58,7 @@ where
|
||||
service: CloneableService<S>,
|
||||
connection: Connection<T, Bytes>,
|
||||
on_connect: Option<Box<dyn DataFactory>>,
|
||||
on_connect_data: Extensions,
|
||||
config: ServiceConfig,
|
||||
timeout: Option<Delay>,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
@ -82,6 +85,7 @@ where
|
||||
peer_addr,
|
||||
connection,
|
||||
on_connect,
|
||||
on_connect_data,
|
||||
ka_expire,
|
||||
ka_timer,
|
||||
_t: PhantomData,
|
||||
@ -130,11 +134,15 @@ where
|
||||
head.headers = parts.headers.into();
|
||||
head.peer_addr = this.peer_addr;
|
||||
|
||||
// DEPRECATED
|
||||
// set on_connect data
|
||||
if let Some(ref on_connect) = this.on_connect {
|
||||
on_connect.set(&mut req.extensions_mut());
|
||||
}
|
||||
|
||||
// merge on_connect_ext data into request extensions
|
||||
req.extensions_mut().drain_from(&mut this.on_connect_data);
|
||||
|
||||
actix_rt::spawn(ServiceResponse::<
|
||||
S::Future,
|
||||
S::Response,
|
||||
@ -227,9 +235,11 @@ where
|
||||
if !has_date {
|
||||
let mut bytes = BytesMut::with_capacity(29);
|
||||
self.config.set_date_header(&mut bytes);
|
||||
res.headers_mut().insert(DATE, unsafe {
|
||||
HeaderValue::from_maybe_shared_unchecked(bytes.freeze())
|
||||
});
|
||||
res.headers_mut().insert(
|
||||
DATE,
|
||||
// SAFETY: serialized date-times are known ASCII strings
|
||||
unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) },
|
||||
);
|
||||
}
|
||||
|
||||
res
|
||||
|
@ -2,7 +2,7 @@ use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{net, rc};
|
||||
use std::{net, rc::Rc};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::net::TcpStream;
|
||||
@ -23,6 +23,7 @@ use crate::error::{DispatchError, Error};
|
||||
use crate::helpers::DataFactory;
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::{ConnectCallback, Extensions};
|
||||
|
||||
use super::dispatcher::Dispatcher;
|
||||
|
||||
@ -30,7 +31,8 @@ use super::dispatcher::Dispatcher;
|
||||
pub struct H2Service<T, S, B> {
|
||||
srv: S,
|
||||
cfg: ServiceConfig,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
}
|
||||
|
||||
@ -50,19 +52,27 @@ where
|
||||
H2Service {
|
||||
cfg,
|
||||
on_connect: None,
|
||||
on_connect_ext: None,
|
||||
srv: service.into_factory(),
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set on connect callback.
|
||||
|
||||
pub(crate) fn on_connect(
|
||||
mut self,
|
||||
f: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
) -> Self {
|
||||
self.on_connect = f;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set on connect callback.
|
||||
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
|
||||
self.on_connect_ext = f;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> H2Service<TcpStream, S, B>
|
||||
@ -97,7 +107,7 @@ where
|
||||
mod openssl {
|
||||
use actix_service::{fn_factory, fn_service};
|
||||
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
|
||||
use actix_tls::{openssl::HandshakeError, SslError};
|
||||
use actix_tls::{openssl::HandshakeError, TlsError};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -117,12 +127,12 @@ mod openssl {
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<HandshakeError<TcpStream>, DispatchError>,
|
||||
Error = TlsError<HandshakeError<TcpStream>, DispatchError>,
|
||||
InitError = S::InitError,
|
||||
> {
|
||||
pipeline_factory(
|
||||
Acceptor::new(acceptor)
|
||||
.map_err(SslError::Ssl)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(fn_factory(|| {
|
||||
@ -131,7 +141,7 @@ mod openssl {
|
||||
ok((io, peer_addr))
|
||||
}))
|
||||
}))
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -140,7 +150,7 @@ mod openssl {
|
||||
mod rustls {
|
||||
use super::*;
|
||||
use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
|
||||
use actix_tls::SslError;
|
||||
use actix_tls::TlsError;
|
||||
use std::io;
|
||||
|
||||
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
|
||||
@ -159,7 +169,7 @@ mod rustls {
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<io::Error, DispatchError>,
|
||||
Error = TlsError<io::Error, DispatchError>,
|
||||
InitError = S::InitError,
|
||||
> {
|
||||
let protos = vec!["h2".to_string().into()];
|
||||
@ -167,7 +177,7 @@ mod rustls {
|
||||
|
||||
pipeline_factory(
|
||||
Acceptor::new(config)
|
||||
.map_err(SslError::Ssl)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(fn_factory(|| {
|
||||
@ -176,7 +186,7 @@ mod rustls {
|
||||
ok((io, peer_addr))
|
||||
}))
|
||||
}))
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -203,6 +213,7 @@ where
|
||||
fut: self.srv.new_service(()),
|
||||
cfg: Some(self.cfg.clone()),
|
||||
on_connect: self.on_connect.clone(),
|
||||
on_connect_ext: self.on_connect_ext.clone(),
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -214,7 +225,8 @@ pub struct H2ServiceResponse<T, S: ServiceFactory, B> {
|
||||
#[pin]
|
||||
fut: S::Future,
|
||||
cfg: Option<ServiceConfig>,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
}
|
||||
|
||||
@ -237,6 +249,7 @@ where
|
||||
H2ServiceHandler::new(
|
||||
this.cfg.take().unwrap(),
|
||||
this.on_connect.clone(),
|
||||
this.on_connect_ext.clone(),
|
||||
service,
|
||||
)
|
||||
}))
|
||||
@ -247,7 +260,8 @@ where
|
||||
pub struct H2ServiceHandler<T, S: Service, B> {
|
||||
srv: CloneableService<S>,
|
||||
cfg: ServiceConfig,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
}
|
||||
|
||||
@ -261,12 +275,14 @@ where
|
||||
{
|
||||
fn new(
|
||||
cfg: ServiceConfig,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
srv: S,
|
||||
) -> H2ServiceHandler<T, S, B> {
|
||||
H2ServiceHandler {
|
||||
cfg,
|
||||
on_connect,
|
||||
on_connect_ext,
|
||||
srv: CloneableService::new(srv),
|
||||
_t: PhantomData,
|
||||
}
|
||||
@ -296,18 +312,21 @@ where
|
||||
}
|
||||
|
||||
fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
|
||||
let on_connect = if let Some(ref on_connect) = self.on_connect {
|
||||
Some(on_connect(&io))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
|
||||
|
||||
let mut connect_extensions = Extensions::new();
|
||||
if let Some(ref handler) = self.on_connect_ext {
|
||||
// run on_connect_ext callback, populating connect extensions
|
||||
handler(&io, &mut connect_extensions);
|
||||
}
|
||||
|
||||
H2ServiceHandlerResponse {
|
||||
state: State::Handshake(
|
||||
Some(self.srv.clone()),
|
||||
Some(self.cfg.clone()),
|
||||
addr,
|
||||
on_connect,
|
||||
deprecated_on_connect,
|
||||
Some(connect_extensions),
|
||||
server::handshake(io),
|
||||
),
|
||||
}
|
||||
@ -325,6 +344,7 @@ where
|
||||
Option<ServiceConfig>,
|
||||
Option<net::SocketAddr>,
|
||||
Option<Box<dyn DataFactory>>,
|
||||
Option<Extensions>,
|
||||
Handshake<T, Bytes>,
|
||||
),
|
||||
}
|
||||
@ -360,6 +380,7 @@ where
|
||||
ref mut config,
|
||||
ref peer_addr,
|
||||
ref mut on_connect,
|
||||
ref mut on_connect_data,
|
||||
ref mut handshake,
|
||||
) => match Pin::new(handshake).poll(cx) {
|
||||
Poll::Ready(Ok(conn)) => {
|
||||
@ -367,6 +388,7 @@ where
|
||||
srv.take().unwrap(),
|
||||
conn,
|
||||
on_connect.take(),
|
||||
on_connect_data.take().unwrap(),
|
||||
config.take().unwrap(),
|
||||
None,
|
||||
*peer_addr,
|
||||
|
@ -283,11 +283,11 @@ impl DispositionParam {
|
||||
/// Some("\u{1f600}.svg".as_bytes()));
|
||||
/// ```
|
||||
///
|
||||
/// # WARN
|
||||
/// # Security Note
|
||||
///
|
||||
/// If "filename" parameter is supplied, do not use the file name blindly, check and possibly
|
||||
/// change to match local file system conventions if applicable, and do not use directory path
|
||||
/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3)
|
||||
/// .
|
||||
/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ContentDisposition {
|
||||
/// The disposition type
|
||||
|
@ -50,6 +50,7 @@ impl<'a> io::Write for Writer<'a> {
|
||||
self.0.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
//! Basic http primitives for actix-net framework.
|
||||
#![warn(rust_2018_idioms, warnings)]
|
||||
//! HTTP primitives for the Actix ecosystem.
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(
|
||||
clippy::type_complexity,
|
||||
clippy::too_many_arguments,
|
||||
clippy::new_without_default,
|
||||
clippy::borrow_interior_mutable_const
|
||||
)]
|
||||
#![allow(clippy::manual_strip)] // Allow this to keep MSRV(1.42).
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
@ -76,3 +80,5 @@ pub enum Protocol {
|
||||
Http1,
|
||||
Http2,
|
||||
}
|
||||
|
||||
type ConnectCallback<IO> = dyn Fn(&IO, &mut Extensions);
|
||||
|
@ -38,7 +38,7 @@ macro_rules! downcast {
|
||||
/// Downcasts generic body to a specific type.
|
||||
pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> {
|
||||
if self.__private_get_type_id__().0 == std::any::TypeId::of::<T>() {
|
||||
// Safety: external crates cannot override the default
|
||||
// SAFETY: external crates cannot override the default
|
||||
// implementation of `__private_get_type_id__`, since
|
||||
// it requires returning a private type. We can therefore
|
||||
// rely on the returned `TypeId`, which ensures that this
|
||||
@ -48,10 +48,11 @@ macro_rules! downcast {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Downcasts a generic body to a mutable specific type.
|
||||
pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> {
|
||||
if self.__private_get_type_id__().0 == std::any::TypeId::of::<T>() {
|
||||
// Safety: external crates cannot override the default
|
||||
// SAFETY: external crates cannot override the default
|
||||
// implementation of `__private_get_type_id__`, since
|
||||
// it requires returning a private type. We can therefore
|
||||
// rely on the returned `TypeId`, which ensures that this
|
||||
@ -86,7 +87,7 @@ mod tests {
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast");
|
||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||
body.push_str("!");
|
||||
body.push('!');
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast!");
|
||||
let not_body = resp_body.downcast_ref::<()>();
|
||||
|
@ -554,8 +554,9 @@ impl ResponseBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// This method calls provided closure with builder reference if value is
|
||||
/// true.
|
||||
/// This method calls provided closure with builder reference if value is `true`.
|
||||
#[doc(hidden)]
|
||||
#[deprecated = "Use an if statement."]
|
||||
pub fn if_true<F>(&mut self, value: bool, f: F) -> &mut Self
|
||||
where
|
||||
F: FnOnce(&mut ResponseBuilder),
|
||||
@ -566,8 +567,9 @@ impl ResponseBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// This method calls provided closure with builder reference if value is
|
||||
/// Some.
|
||||
/// This method calls provided closure with builder reference if value is `Some`.
|
||||
#[doc(hidden)]
|
||||
#[deprecated = "Use an if-let construction."]
|
||||
pub fn if_some<T, F>(&mut self, value: Option<T>, f: F) -> &mut Self
|
||||
where
|
||||
F: FnOnce(T, &mut ResponseBuilder),
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{fmt, net, rc};
|
||||
use std::{fmt, net, rc::Rc};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_rt::net::TcpStream;
|
||||
@ -20,15 +20,17 @@ use crate::error::{DispatchError, Error};
|
||||
use crate::helpers::DataFactory;
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::{h1, h2::Dispatcher, Protocol};
|
||||
use crate::{h1, h2::Dispatcher, ConnectCallback, Extensions, Protocol};
|
||||
|
||||
/// `ServiceFactory` HTTP1.1/HTTP2 transport implementation
|
||||
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
|
||||
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler<T>> {
|
||||
srv: S,
|
||||
cfg: ServiceConfig,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
// DEPRECATED: in favor of on_connect_ext
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
}
|
||||
|
||||
@ -66,6 +68,7 @@ where
|
||||
expect: h1::ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect: None,
|
||||
on_connect_ext: None,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -81,6 +84,7 @@ where
|
||||
expect: h1::ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect: None,
|
||||
on_connect_ext: None,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -113,6 +117,7 @@ where
|
||||
srv: self.srv,
|
||||
upgrade: self.upgrade,
|
||||
on_connect: self.on_connect,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -138,6 +143,7 @@ where
|
||||
srv: self.srv,
|
||||
expect: self.expect,
|
||||
on_connect: self.on_connect,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -145,11 +151,17 @@ where
|
||||
/// Set on connect callback.
|
||||
pub(crate) fn on_connect(
|
||||
mut self,
|
||||
f: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
) -> Self {
|
||||
self.on_connect = f;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set connect callback with mutable access to request data container.
|
||||
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
|
||||
self.on_connect_ext = f;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B, X, U> HttpService<TcpStream, S, B, X, U>
|
||||
@ -195,7 +207,7 @@ where
|
||||
mod openssl {
|
||||
use super::*;
|
||||
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
|
||||
use actix_tls::{openssl::HandshakeError, SslError};
|
||||
use actix_tls::{openssl::HandshakeError, TlsError};
|
||||
|
||||
impl<S, B, X, U> HttpService<SslStream<TcpStream>, S, B, X, U>
|
||||
where
|
||||
@ -226,12 +238,12 @@ mod openssl {
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<HandshakeError<TcpStream>, DispatchError>,
|
||||
Error = TlsError<HandshakeError<TcpStream>, DispatchError>,
|
||||
InitError = (),
|
||||
> {
|
||||
pipeline_factory(
|
||||
Acceptor::new(acceptor)
|
||||
.map_err(SslError::Ssl)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(|io: SslStream<TcpStream>| {
|
||||
@ -247,7 +259,7 @@ mod openssl {
|
||||
let peer_addr = io.get_ref().peer_addr().ok();
|
||||
ok((io, proto, peer_addr))
|
||||
})
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,7 +268,7 @@ mod openssl {
|
||||
mod rustls {
|
||||
use super::*;
|
||||
use actix_tls::rustls::{Acceptor, ServerConfig, Session, TlsStream};
|
||||
use actix_tls::SslError;
|
||||
use actix_tls::TlsError;
|
||||
use std::io;
|
||||
|
||||
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
|
||||
@ -288,7 +300,7 @@ mod rustls {
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<io::Error, DispatchError>,
|
||||
Error = TlsError<io::Error, DispatchError>,
|
||||
InitError = (),
|
||||
> {
|
||||
let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()];
|
||||
@ -296,7 +308,7 @@ mod rustls {
|
||||
|
||||
pipeline_factory(
|
||||
Acceptor::new(config)
|
||||
.map_err(SslError::Ssl)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(|io: TlsStream<TcpStream>| {
|
||||
@ -312,7 +324,7 @@ mod rustls {
|
||||
let peer_addr = io.get_ref().0.peer_addr().ok();
|
||||
ok((io, proto, peer_addr))
|
||||
})
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -355,6 +367,7 @@ where
|
||||
expect: None,
|
||||
upgrade: None,
|
||||
on_connect: self.on_connect.clone(),
|
||||
on_connect_ext: self.on_connect_ext.clone(),
|
||||
cfg: self.cfg.clone(),
|
||||
_t: PhantomData,
|
||||
}
|
||||
@ -378,7 +391,8 @@ pub struct HttpServiceResponse<
|
||||
fut_upg: Option<U::Future>,
|
||||
expect: Option<X::Service>,
|
||||
upgrade: Option<U::Service>,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
cfg: ServiceConfig,
|
||||
_t: PhantomData<(T, B)>,
|
||||
}
|
||||
@ -429,6 +443,7 @@ where
|
||||
.fut
|
||||
.poll(cx)
|
||||
.map_err(|e| log::error!("Init http service error: {:?}", e)));
|
||||
|
||||
Poll::Ready(result.map(|service| {
|
||||
let this = self.as_mut().project();
|
||||
HttpServiceHandler::new(
|
||||
@ -437,6 +452,7 @@ where
|
||||
this.expect.take().unwrap(),
|
||||
this.upgrade.take(),
|
||||
this.on_connect.clone(),
|
||||
this.on_connect_ext.clone(),
|
||||
)
|
||||
}))
|
||||
}
|
||||
@ -448,7 +464,8 @@ pub struct HttpServiceHandler<T, S: Service, B, X: Service, U: Service> {
|
||||
expect: CloneableService<X>,
|
||||
upgrade: Option<CloneableService<U>>,
|
||||
cfg: ServiceConfig,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_t: PhantomData<(T, B, X)>,
|
||||
}
|
||||
|
||||
@ -469,11 +486,13 @@ where
|
||||
srv: S,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
) -> HttpServiceHandler<T, S, B, X, U> {
|
||||
HttpServiceHandler {
|
||||
cfg,
|
||||
on_connect,
|
||||
on_connect_ext,
|
||||
srv: CloneableService::new(srv),
|
||||
expect: CloneableService::new(expect),
|
||||
upgrade: upgrade.map(CloneableService::new),
|
||||
@ -543,11 +562,12 @@ where
|
||||
}
|
||||
|
||||
fn call(&mut self, (io, proto, peer_addr): Self::Request) -> Self::Future {
|
||||
let on_connect = if let Some(ref on_connect) = self.on_connect {
|
||||
Some(on_connect(&io))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut connect_extensions = Extensions::new();
|
||||
|
||||
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
|
||||
if let Some(ref handler) = self.on_connect_ext {
|
||||
handler(&io, &mut connect_extensions);
|
||||
}
|
||||
|
||||
match proto {
|
||||
Protocol::Http2 => HttpServiceHandlerResponse {
|
||||
@ -555,10 +575,12 @@ where
|
||||
server::handshake(io),
|
||||
self.cfg.clone(),
|
||||
self.srv.clone(),
|
||||
on_connect,
|
||||
deprecated_on_connect,
|
||||
connect_extensions,
|
||||
peer_addr,
|
||||
))),
|
||||
},
|
||||
|
||||
Protocol::Http1 => HttpServiceHandlerResponse {
|
||||
state: State::H1(h1::Dispatcher::new(
|
||||
io,
|
||||
@ -566,7 +588,8 @@ where
|
||||
self.srv.clone(),
|
||||
self.expect.clone(),
|
||||
self.upgrade.clone(),
|
||||
on_connect,
|
||||
deprecated_on_connect,
|
||||
connect_extensions,
|
||||
peer_addr,
|
||||
)),
|
||||
},
|
||||
@ -595,6 +618,7 @@ where
|
||||
ServiceConfig,
|
||||
CloneableService<S>,
|
||||
Option<Box<dyn DataFactory>>,
|
||||
Extensions,
|
||||
Option<net::SocketAddr>,
|
||||
)>,
|
||||
),
|
||||
@ -670,9 +694,16 @@ where
|
||||
} else {
|
||||
panic!()
|
||||
};
|
||||
let (_, cfg, srv, on_connect, peer_addr) = data.take().unwrap();
|
||||
let (_, cfg, srv, on_connect, on_connect_data, peer_addr) =
|
||||
data.take().unwrap();
|
||||
self.set(State::H2(Dispatcher::new(
|
||||
srv, conn, on_connect, cfg, None, peer_addr,
|
||||
srv,
|
||||
conn,
|
||||
on_connect,
|
||||
on_connect_data,
|
||||
cfg,
|
||||
None,
|
||||
peer_addr,
|
||||
)));
|
||||
self.poll(cx)
|
||||
}
|
||||
|
@ -91,8 +91,7 @@ impl Codec {
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for Codec {
|
||||
type Item = Message;
|
||||
impl Encoder<Message> for Codec {
|
||||
type Error = ProtocolError;
|
||||
|
||||
fn encode(&mut self, item: Message, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
|
@ -4,16 +4,18 @@ use std::task::{Context, Poll};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_service::{IntoService, Service};
|
||||
use actix_utils::framed;
|
||||
use actix_utils::dispatcher::{Dispatcher as InnerDispatcher, DispatcherError};
|
||||
|
||||
use super::{Codec, Frame, Message};
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pub struct Dispatcher<S, T>
|
||||
where
|
||||
S: Service<Request = Frame, Response = Message> + 'static,
|
||||
T: AsyncRead + AsyncWrite,
|
||||
{
|
||||
inner: framed::Dispatcher<S, T, Codec>,
|
||||
#[pin]
|
||||
inner: InnerDispatcher<S, T, Codec, Message>,
|
||||
}
|
||||
|
||||
impl<S, T> Dispatcher<S, T>
|
||||
@ -25,13 +27,13 @@ where
|
||||
{
|
||||
pub fn new<F: IntoService<S>>(io: T, service: F) -> Self {
|
||||
Dispatcher {
|
||||
inner: framed::Dispatcher::new(Framed::new(io, Codec::new()), service),
|
||||
inner: InnerDispatcher::new(Framed::new(io, Codec::new()), service),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with<F: IntoService<S>>(framed: Framed<T, Codec>, service: F) -> Self {
|
||||
Dispatcher {
|
||||
inner: framed::Dispatcher::new(framed, service),
|
||||
inner: InnerDispatcher::new(framed, service),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,9 +45,9 @@ where
|
||||
S::Future: 'static,
|
||||
S::Error: 'static,
|
||||
{
|
||||
type Output = Result<(), framed::DispatcherError<S::Error, Codec>>;
|
||||
type Output = Result<(), DispatcherError<S::Error, Codec, Message>>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.inner).poll(cx)
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
self.project().inner.poll(cx)
|
||||
}
|
||||
}
|
||||
|
@ -4,16 +4,21 @@ use std::ptr::copy_nonoverlapping;
|
||||
use std::slice;
|
||||
|
||||
// Holds a slice guaranteed to be shorter than 8 bytes
|
||||
struct ShortSlice<'a>(&'a mut [u8]);
|
||||
struct ShortSlice<'a> {
|
||||
inner: &'a mut [u8],
|
||||
}
|
||||
|
||||
impl<'a> ShortSlice<'a> {
|
||||
/// # Safety
|
||||
/// Given slice must be shorter than 8 bytes.
|
||||
unsafe fn new(slice: &'a mut [u8]) -> Self {
|
||||
// Sanity check for debug builds
|
||||
debug_assert!(slice.len() < 8);
|
||||
ShortSlice(slice)
|
||||
ShortSlice { inner: slice }
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
self.inner.len()
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,15 +51,15 @@ pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
// TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so
|
||||
// inefficient, it could be done better. The compiler does not understand that
|
||||
// a `ShortSlice` must be smaller than a u64.
|
||||
#[inline]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn xor_short(buf: ShortSlice<'_>, mask: u64) {
|
||||
// Unsafe: we know that a `ShortSlice` fits in a u64
|
||||
// SAFETY: we know that a `ShortSlice` fits in a u64
|
||||
unsafe {
|
||||
let (ptr, len) = (buf.0.as_mut_ptr(), buf.0.len());
|
||||
let (ptr, len) = (buf.inner.as_mut_ptr(), buf.len());
|
||||
let mut b: u64 = 0;
|
||||
#[allow(trivial_casts)]
|
||||
copy_nonoverlapping(ptr, &mut b as *mut _ as *mut u8, len);
|
||||
@ -64,8 +69,9 @@ fn xor_short(buf: ShortSlice<'_>, mask: u64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must ensure the buffer has the correct size and alignment.
|
||||
#[inline]
|
||||
// Unsafe: caller must ensure the buffer has the correct size and alignment
|
||||
unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] {
|
||||
// Assert correct size and alignment in debug builds
|
||||
debug_assert!(buf.len().trailing_zeros() >= 3);
|
||||
@ -74,9 +80,9 @@ unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] {
|
||||
slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u64, buf.len() >> 3)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
// Splits a slice into three parts: an unaligned short head and tail, plus an aligned
|
||||
// u64 mid section.
|
||||
#[inline]
|
||||
fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) {
|
||||
let start_ptr = buf.as_ptr() as usize;
|
||||
let end_ptr = start_ptr + buf.len();
|
||||
@ -91,13 +97,19 @@ fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) {
|
||||
let (tmp, tail) = buf.split_at_mut(end_aligned - start_ptr);
|
||||
let (head, mid) = tmp.split_at_mut(start_aligned - start_ptr);
|
||||
|
||||
// Unsafe: we know the middle section is correctly aligned, and the outer
|
||||
// SAFETY: we know the middle section is correctly aligned, and the outer
|
||||
// sections are smaller than 8 bytes
|
||||
unsafe { (ShortSlice::new(head), cast_slice(mid), ShortSlice(tail)) }
|
||||
unsafe {
|
||||
(
|
||||
ShortSlice::new(head),
|
||||
cast_slice(mid),
|
||||
ShortSlice::new(tail),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// We didn't cross even one aligned boundary!
|
||||
|
||||
// Unsafe: The outer sections are smaller than 8 bytes
|
||||
// SAFETY: The outer sections are smaller than 8 bytes
|
||||
unsafe { (ShortSlice::new(buf), &mut [], ShortSlice::new(&mut [])) }
|
||||
}
|
||||
}
|
||||
|
@ -411,8 +411,10 @@ async fn test_h2_on_connect() {
|
||||
let srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.on_connect(|_| 10usize)
|
||||
.on_connect_ext(|_, data| data.insert(20isize))
|
||||
.h2(|req: Request| {
|
||||
assert!(req.extensions().contains::<usize>());
|
||||
assert!(req.extensions().contains::<isize>());
|
||||
ok::<_, ()>(Response::Ok().finish())
|
||||
})
|
||||
.openssl(ssl_acceptor())
|
||||
|
@ -663,8 +663,10 @@ async fn test_h1_on_connect() {
|
||||
let srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.on_connect(|_| 10usize)
|
||||
.on_connect_ext(|_, data| data.insert(20isize))
|
||||
.h1(|req: Request| {
|
||||
assert!(req.extensions().contains::<usize>());
|
||||
assert!(req.extensions().contains::<isize>());
|
||||
future::ok::<_, ()>(Response::Ok().finish())
|
||||
})
|
||||
.tcp()
|
||||
|
@ -8,7 +8,7 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_http::{body, h1, ws, Error, HttpService, Request, Response};
|
||||
use actix_http_test::test_server;
|
||||
use actix_service::{fn_factory, Service};
|
||||
use actix_utils::framed::Dispatcher;
|
||||
use actix_utils::dispatcher::Dispatcher;
|
||||
use bytes::Bytes;
|
||||
use futures_util::future;
|
||||
use futures_util::task::{Context, Poll};
|
||||
@ -59,7 +59,7 @@ where
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Dispatcher::new(framed.into_framed(ws::Codec::new()), service)
|
||||
Dispatcher::new(framed.replace_codec(ws::Codec::new()), service)
|
||||
.await
|
||||
.map_err(|_| panic!())
|
||||
};
|
||||
|
@ -1,11 +0,0 @@
|
||||
# Identity service for actix web framework [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-identity) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
**This crate moved to https://github.com/actix/actix-extras.**
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-identity/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-session](https://crates.io/crates/actix-identity)
|
||||
* Minimum supported Rust version: 1.34 or later
|
@ -1,6 +1,15 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
* Fix multipart consuming payload before header checks #1513
|
||||
|
||||
|
||||
## 3.0.0 - 2020-09-11
|
||||
* No significant changes from `3.0.0-beta.2`.
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2020-09-10
|
||||
* Update `actix-*` dependencies to latest versions.
|
||||
|
||||
|
||||
## 0.3.0-beta.1 - 2020-07-15
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.3.0-beta.1"
|
||||
version = "0.3.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Multipart support for actix web framework."
|
||||
readme = "README.md"
|
||||
@ -16,9 +16,9 @@ name = "actix_multipart"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "3.0.0-beta.1", default-features = false }
|
||||
actix-service = "1.0.1"
|
||||
actix-utils = "1.0.3"
|
||||
actix-web = { version = "3.0.0", default-features = false }
|
||||
actix-service = "1.0.6"
|
||||
actix-utils = "2.0.0"
|
||||
bytes = "0.5.3"
|
||||
derive_more = "0.99.2"
|
||||
httparse = "1.3"
|
||||
@ -29,4 +29,4 @@ twoway = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-http = "2.0.0-beta.1"
|
||||
actix-http = "2.0.0"
|
||||
|
@ -36,6 +36,9 @@ impl FromRequest for Multipart {
|
||||
|
||||
#[inline]
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
ok(Multipart::new(req.headers(), payload.take()))
|
||||
ok(match Multipart::boundary(req.headers()) {
|
||||
Ok(boundary) => Multipart::from_boundary(boundary, payload.take()),
|
||||
Err(err) => Multipart::from_error(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
//! Multipart form support for Actix web.
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(clippy::borrow_interior_mutable_const)]
|
||||
|
||||
mod error;
|
||||
|
@ -1,4 +1,5 @@
|
||||
//! Multipart payload support
|
||||
|
||||
use std::cell::{Cell, RefCell, RefMut};
|
||||
use std::convert::TryFrom;
|
||||
use std::marker::PhantomData;
|
||||
@ -63,26 +64,13 @@ impl Multipart {
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static,
|
||||
{
|
||||
match Self::boundary(headers) {
|
||||
Ok(boundary) => Multipart {
|
||||
error: None,
|
||||
safety: Safety::new(),
|
||||
inner: Some(Rc::new(RefCell::new(InnerMultipart {
|
||||
boundary,
|
||||
payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))),
|
||||
state: InnerState::FirstBoundary,
|
||||
item: InnerMultipartItem::None,
|
||||
}))),
|
||||
},
|
||||
Err(err) => Multipart {
|
||||
error: Some(err),
|
||||
safety: Safety::new(),
|
||||
inner: None,
|
||||
},
|
||||
Ok(boundary) => Multipart::from_boundary(boundary, stream),
|
||||
Err(err) => Multipart::from_error(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract boundary info from headers.
|
||||
fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
|
||||
pub(crate) fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
|
||||
if let Some(content_type) = headers.get(&header::CONTENT_TYPE) {
|
||||
if let Ok(content_type) = content_type.to_str() {
|
||||
if let Ok(ct) = content_type.parse::<mime::Mime>() {
|
||||
@ -101,6 +89,32 @@ impl Multipart {
|
||||
Err(MultipartError::NoContentType)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create multipart instance for given boundary and stream
|
||||
pub(crate) fn from_boundary<S>(boundary: String, stream: S) -> Multipart
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static,
|
||||
{
|
||||
Multipart {
|
||||
error: None,
|
||||
safety: Safety::new(),
|
||||
inner: Some(Rc::new(RefCell::new(InnerMultipart {
|
||||
boundary,
|
||||
payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))),
|
||||
state: InnerState::FirstBoundary,
|
||||
item: InnerMultipartItem::None,
|
||||
}))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create Multipart instance from MultipartError
|
||||
pub(crate) fn from_error(err: MultipartError) -> Multipart {
|
||||
Multipart {
|
||||
error: Some(err),
|
||||
safety: Safety::new(),
|
||||
inner: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Multipart {
|
||||
@ -108,7 +122,7 @@ impl Stream for Multipart {
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
if let Some(err) = self.error.take() {
|
||||
Poll::Ready(Some(Err(err)))
|
||||
@ -244,7 +258,7 @@ impl InnerMultipart {
|
||||
fn poll(
|
||||
&mut self,
|
||||
safety: &Safety,
|
||||
cx: &mut Context,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Field, MultipartError>>> {
|
||||
if self.state == InnerState::Eof {
|
||||
Poll::Ready(None)
|
||||
@ -416,7 +430,10 @@ impl Field {
|
||||
impl Stream for Field {
|
||||
type Item = Result<Bytes, MultipartError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
if self.safety.current() {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if let Some(mut payload) =
|
||||
@ -434,7 +451,7 @@ impl Stream for Field {
|
||||
}
|
||||
|
||||
impl fmt::Debug for Field {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "\nField: {}", self.ct)?;
|
||||
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
||||
writeln!(f, " headers:")?;
|
||||
@ -689,7 +706,7 @@ impl Safety {
|
||||
self.clean.get()
|
||||
}
|
||||
|
||||
fn clone(&self, cx: &mut Context) -> Safety {
|
||||
fn clone(&self, cx: &mut Context<'_>) -> Safety {
|
||||
let payload = Rc::clone(&self.payload);
|
||||
let s = Safety {
|
||||
task: LocalWaker::new(),
|
||||
@ -734,7 +751,7 @@ impl PayloadBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_stream(&mut self, cx: &mut Context) -> Result<(), PayloadError> {
|
||||
fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> {
|
||||
loop {
|
||||
match Pin::new(&mut self.stream).poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data),
|
||||
@ -811,6 +828,8 @@ mod tests {
|
||||
use actix_http::h1::Payload;
|
||||
use actix_utils::mpsc;
|
||||
use actix_web::http::header::{DispositionParam, DispositionType};
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::FromRequest;
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::lazy;
|
||||
|
||||
@ -887,7 +906,7 @@ mod tests {
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
if !this.ready {
|
||||
@ -1147,4 +1166,38 @@ mod tests {
|
||||
);
|
||||
assert_eq!(payload.buf.len(), 0);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_multipart_from_error() {
|
||||
let err = MultipartError::NoContentType;
|
||||
let mut multipart = Multipart::from_error(err);
|
||||
assert!(multipart.next().await.unwrap().is_err())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_multipart_from_boundary() {
|
||||
let (_, payload) = create_stream();
|
||||
let (_, headers) = create_simple_request_with_header();
|
||||
let boundary = Multipart::boundary(&headers);
|
||||
assert!(boundary.is_ok());
|
||||
let _ = Multipart::from_boundary(boundary.unwrap(), payload);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_multipart_payload_consumption() {
|
||||
// with sample payload and HttpRequest with no headers
|
||||
let (_, inner_payload) = Payload::create(false);
|
||||
let mut payload = actix_web::dev::Payload::from(inner_payload);
|
||||
let req = TestRequest::default().to_http_request();
|
||||
|
||||
// multipart should generate an error
|
||||
let mut mp = Multipart::from_request(&req, &mut payload).await.unwrap();
|
||||
assert!(mp.next().await.unwrap().is_err());
|
||||
|
||||
// and should not consume the payload
|
||||
match payload {
|
||||
actix_web::dev::Payload::H1(_) => {} //expected
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
# Session for actix web framework [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-session) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
**This crate moved to https://github.com/actix/actix-extras.**
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-session/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-session](https://crates.io/crates/actix-session)
|
||||
* Minimum supported Rust version: 1.34 or later
|
@ -1,6 +1,14 @@
|
||||
# Changes
|
||||
|
||||
## [Unreleased] - 2020-xx-xx
|
||||
## Unreleased - 2020-xx-xx
|
||||
* Upgrade `pin-project` to `1.0`.
|
||||
|
||||
## 3.0.0 - 2020-09-11
|
||||
* No significant changes from `3.0.0-beta.2`.
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2020-09-10
|
||||
* Update `actix-*` dependencies to latest versions.
|
||||
|
||||
|
||||
## [3.0.0-beta.1] - 2020-xx-xx
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web-actors"
|
||||
version = "3.0.0-beta.1"
|
||||
version = "3.0.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix actors support for actix web framework."
|
||||
readme = "README.md"
|
||||
@ -16,16 +16,16 @@ name = "actix_web_actors"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix = "0.10.0-alpha.2"
|
||||
actix-web = { version = "3.0.0-beta.1", default-features = false }
|
||||
actix-http = "2.0.0-beta.1"
|
||||
actix-codec = "0.2.0"
|
||||
actix = "0.10.0"
|
||||
actix-web = { version = "3.0.0", default-features = false }
|
||||
actix-http = "2.0.0"
|
||||
actix-codec = "0.3.0"
|
||||
bytes = "0.5.2"
|
||||
futures-channel = { version = "0.3.5", default-features = false }
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
pin-project = "0.4.17"
|
||||
pin-project = "1.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-rt = "1.1.1"
|
||||
env_logger = "0.7"
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
|
@ -1,5 +1,8 @@
|
||||
#![allow(clippy::borrow_interior_mutable_const)]
|
||||
//! Actix actors integration for Actix web framework
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(clippy::borrow_interior_mutable_const)]
|
||||
|
||||
mod context;
|
||||
pub mod ws;
|
||||
|
||||
|
@ -3,53 +3,66 @@
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 0.4.0 - 2020-09-20
|
||||
* Added compile success and failure testing. [#1677]
|
||||
* Add `route` macro for supporting multiple HTTP methods guards. [#1674]
|
||||
|
||||
[#1677]: https://github.com/actix/actix-web/pull/1677
|
||||
[#1674]: https://github.com/actix/actix-web/pull/1674
|
||||
|
||||
|
||||
## 0.3.0 - 2020-09-11
|
||||
* No significant changes from `0.3.0-beta.1`.
|
||||
|
||||
|
||||
## 0.3.0-beta.1 - 2020-07-14
|
||||
* Add main entry-point macro that uses re-exported runtime. [#1559]
|
||||
|
||||
[#1559]: https://github.com/actix/actix-web/pull/1559
|
||||
|
||||
|
||||
## [0.2.2] - 2020-05-23
|
||||
## 0.2.2 - 2020-05-23
|
||||
* Add resource middleware on actix-web-codegen [#1467]
|
||||
|
||||
[#1467]: https://github.com/actix/actix-web/pull/1467
|
||||
|
||||
## [0.2.1] - 2020-02-25
|
||||
|
||||
## 0.2.1 - 2020-02-25
|
||||
* Add `#[allow(missing_docs)]` attribute to generated structs [#1368]
|
||||
* Allow the handler function to be named as `config` [#1290]
|
||||
|
||||
[#1368]: https://github.com/actix/actix-web/issues/1368
|
||||
[#1290]: https://github.com/actix/actix-web/issues/1290
|
||||
|
||||
## [0.2.0] - 2019-12-13
|
||||
|
||||
## 0.2.0 - 2019-12-13
|
||||
* Generate code for actix-web 2.0
|
||||
|
||||
## [0.1.3] - 2019-10-14
|
||||
|
||||
## 0.1.3 - 2019-10-14
|
||||
* Bump up `syn` & `quote` to 1.0
|
||||
* Provide better error message
|
||||
|
||||
## [0.1.2] - 2019-06-04
|
||||
|
||||
## 0.1.2 - 2019-06-04
|
||||
* Add macros for head, options, trace, connect and patch http methods
|
||||
|
||||
## [0.1.1] - 2019-06-01
|
||||
|
||||
## 0.1.1 - 2019-06-01
|
||||
* Add syn "extra-traits" feature
|
||||
|
||||
## [0.1.0] - 2019-05-18
|
||||
|
||||
## 0.1.0 - 2019-05-18
|
||||
* Release
|
||||
|
||||
## [0.1.0-beta.1] - 2019-04-20
|
||||
|
||||
## 0.1.0-beta.1 - 2019-04-20
|
||||
* Gen code for actix-web 1.0.0-beta.1
|
||||
|
||||
## [0.1.0-alpha.6] - 2019-04-14
|
||||
|
||||
## 0.1.0-alpha.6 - 2019-04-14
|
||||
* Gen code for actix-web 1.0.0-alpha.6
|
||||
|
||||
## [0.1.0-alpha.1] - 2019-03-28
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
* Initial impl
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web-codegen"
|
||||
version = "0.3.0-beta.1"
|
||||
version = "0.4.0"
|
||||
description = "Actix web proc macros"
|
||||
readme = "README.md"
|
||||
homepage = "https://actix.rs"
|
||||
@ -19,6 +19,8 @@ syn = { version = "1", features = ["full", "parsing"] }
|
||||
proc-macro2 = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-web = "3.0.0-beta.1"
|
||||
actix-rt = "1.1.1"
|
||||
actix-web = "3.0.0"
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
trybuild = "1"
|
||||
rustversion = "1"
|
||||
|
@ -1,8 +1,22 @@
|
||||
# Helper and convenience macros for Actix-web. [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-web-codegen) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# actix-web-codegen
|
||||
|
||||
> Helper and convenience macros for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-web-codegen)
|
||||
[](https://docs.rs/actix-web)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
|
||||
[](https://travis-ci.org/actix/actix-web)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
* [API Documentation](https://docs.rs/actix-web-codegen/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen)
|
||||
* Minimum supported Rust version: 1.40 or later
|
||||
- [API Documentation](https://docs.rs/actix-web-codegen)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen)
|
||||
- Minimum supported Rust version: 1.42 or later.
|
||||
|
||||
## Compile Testing
|
||||
Uses the [`trybuild`] crate. All compile fail tests should include a stderr file generated by `trybuild`. See the [workflow section](https://github.com/dtolnay/trybuild#workflow) of the trybuild docs for info on how to do this.
|
||||
|
||||
[`trybuild`]: https://github.com/dtolnay/trybuild
|
||||
|
@ -1,158 +1,174 @@
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
//! Helper and convenience macros for Actix-web.
|
||||
//! Macros for reducing boilerplate code in Actix Web applications.
|
||||
//!
|
||||
//! ## Runtime Setup
|
||||
//! ## Actix Web Re-exports
|
||||
//! Actix Web re-exports a version of this crate in it's entirety so you usually don't have to
|
||||
//! specify a dependency on this crate explicitly. Sometimes, however, updates are made to this
|
||||
//! crate before the actix-web dependency is updated. Therefore, code examples here will show
|
||||
//! explicit imports. Check the latest [actix-web attributes docs] to see which macros
|
||||
//! are re-exported.
|
||||
//!
|
||||
//! - [main](attr.main.html)
|
||||
//!
|
||||
//! ## Resource Macros:
|
||||
//!
|
||||
//! - [get](attr.get.html)
|
||||
//! - [post](attr.post.html)
|
||||
//! - [put](attr.put.html)
|
||||
//! - [delete](attr.delete.html)
|
||||
//! - [head](attr.head.html)
|
||||
//! - [connect](attr.connect.html)
|
||||
//! - [options](attr.options.html)
|
||||
//! - [trace](attr.trace.html)
|
||||
//! - [patch](attr.patch.html)
|
||||
//!
|
||||
//! ### Attributes:
|
||||
//!
|
||||
//! - `"path"` - Raw literal string with path for which to register handle. Mandatory.
|
||||
//! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
|
||||
//! - `wrap="Middleware"` - Registers a resource middleware.
|
||||
//!
|
||||
//! ### Notes
|
||||
//!
|
||||
//! Function name can be specified as any expression that is going to be accessible to the generate
|
||||
//! code (e.g `my_guard` or `my_module::my_guard`)
|
||||
//!
|
||||
//! ### Example:
|
||||
//! # Runtime Setup
|
||||
//! Used for setting up the actix async runtime. See [main] macro docs.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use actix_web::HttpResponse;
|
||||
//! use actix_web_codegen::get;
|
||||
//!
|
||||
//! #[get("/test")]
|
||||
//! async fn async_test() -> Result<HttpResponse, actix_web::Error> {
|
||||
//! Ok(HttpResponse::Ok().finish())
|
||||
//! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps
|
||||
//! async fn main() {
|
||||
//! async { println!("Hello world"); }.await
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Single Method Handler
|
||||
//! There is a macro to set up a handler for each of the most common HTTP methods that also define
|
||||
//! additional guards and route-specific middleware.
|
||||
//!
|
||||
//! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE]
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use actix_web::HttpResponse;
|
||||
//! # use actix_web_codegen::get;
|
||||
//! #[get("/test")]
|
||||
//! async fn get_handler() -> HttpResponse {
|
||||
//! HttpResponse::Ok().finish()
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Multiple Method Handlers
|
||||
//! Similar to the single method handler macro but takes one or more arguments for the HTTP methods
|
||||
//! it should respond to. See [route] macro docs.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use actix_web::HttpResponse;
|
||||
//! # use actix_web_codegen::route;
|
||||
//! #[route("/test", method="GET", method="HEAD")]
|
||||
//! async fn get_and_head_handler() -> HttpResponse {
|
||||
//! HttpResponse::Ok().finish()
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes
|
||||
//! [main]: attr.main.html
|
||||
//! [route]: attr.route.html
|
||||
//! [GET]: attr.get.html
|
||||
//! [POST]: attr.post.html
|
||||
//! [PUT]: attr.put.html
|
||||
//! [DELETE]: attr.delete.html
|
||||
//! [HEAD]: attr.head.html
|
||||
//! [CONNECT]: attr.connect.html
|
||||
//! [OPTIONS]: attr.options.html
|
||||
//! [TRACE]: attr.trace.html
|
||||
//! [PATCH]: attr.patch.html
|
||||
|
||||
extern crate proc_macro;
|
||||
|
||||
mod route;
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
/// Creates route handler with `GET` method guard.
|
||||
mod route;
|
||||
|
||||
/// Creates resource handler, allowing multiple HTTP method guards.
|
||||
///
|
||||
/// Syntax: `#[get("path"[, attributes])]`
|
||||
/// # Syntax
|
||||
/// ```text
|
||||
/// #[route("path", method="HTTP_METHOD"[, attributes])]
|
||||
/// ```
|
||||
///
|
||||
/// ## Attributes:
|
||||
///
|
||||
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
|
||||
/// # Attributes
|
||||
/// - `"path"` - Raw literal string with path for which to register handler.
|
||||
/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example.
|
||||
/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
|
||||
/// - `wrap="Middleware"` - Registers a resource middleware.
|
||||
///
|
||||
/// # Notes
|
||||
/// Function name can be specified as any expression that is going to be accessible to the generate
|
||||
/// code, e.g `my_guard` or `my_module::my_guard`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use actix_web::HttpResponse;
|
||||
/// # use actix_web_codegen::route;
|
||||
/// #[route("/test", method="GET", method="HEAD")]
|
||||
/// async fn example() -> HttpResponse {
|
||||
/// HttpResponse::Ok().finish()
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Get)
|
||||
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::with_method(None, args, input)
|
||||
}
|
||||
|
||||
/// Creates route handler with `POST` method guard.
|
||||
///
|
||||
/// Syntax: `#[post("path"[, attributes])]`
|
||||
///
|
||||
/// Attributes are the same as in [get](attr.get.html)
|
||||
#[proc_macro_attribute]
|
||||
pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Post)
|
||||
macro_rules! doc_comment {
|
||||
($x:expr; $($tt:tt)*) => {
|
||||
#[doc = $x]
|
||||
$($tt)*
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates route handler with `PUT` method guard.
|
||||
///
|
||||
/// Syntax: `#[put("path"[, attributes])]`
|
||||
///
|
||||
/// Attributes are the same as in [get](attr.get.html)
|
||||
#[proc_macro_attribute]
|
||||
pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Put)
|
||||
macro_rules! method_macro {
|
||||
(
|
||||
$($variant:ident, $method:ident,)+
|
||||
) => {
|
||||
$(doc_comment! {
|
||||
concat!("
|
||||
Creates route handler with `actix_web::guard::", stringify!($variant), "`.
|
||||
|
||||
# Syntax
|
||||
```text
|
||||
#[", stringify!($method), r#"("path"[, attributes])]
|
||||
```
|
||||
|
||||
# Attributes
|
||||
- `"path"` - Raw literal string with path for which to register handler.
|
||||
- `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`.
|
||||
- `wrap="Middleware"` - Registers a resource middleware.
|
||||
|
||||
# Notes
|
||||
Function name can be specified as any expression that is going to be accessible to the generate
|
||||
code, e.g `my_guard` or `my_module::my_guard`.
|
||||
|
||||
# Example
|
||||
|
||||
```rust
|
||||
# use actix_web::HttpResponse;
|
||||
# use actix_web_codegen::"#, stringify!($method), ";
|
||||
#[", stringify!($method), r#"("/")]
|
||||
async fn example() -> HttpResponse {
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
```
|
||||
"#);
|
||||
#[proc_macro_attribute]
|
||||
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::with_method(Some(route::MethodType::$variant), args, input)
|
||||
}
|
||||
})+
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates route handler with `DELETE` method guard.
|
||||
///
|
||||
/// Syntax: `#[delete("path"[, attributes])]`
|
||||
///
|
||||
/// Attributes are the same as in [get](attr.get.html)
|
||||
#[proc_macro_attribute]
|
||||
pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Delete)
|
||||
}
|
||||
|
||||
/// Creates route handler with `HEAD` method guard.
|
||||
///
|
||||
/// Syntax: `#[head("path"[, attributes])]`
|
||||
///
|
||||
/// Attributes are the same as in [head](attr.head.html)
|
||||
#[proc_macro_attribute]
|
||||
pub fn head(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Head)
|
||||
}
|
||||
|
||||
/// Creates route handler with `CONNECT` method guard.
|
||||
///
|
||||
/// Syntax: `#[connect("path"[, attributes])]`
|
||||
///
|
||||
/// Attributes are the same as in [connect](attr.connect.html)
|
||||
#[proc_macro_attribute]
|
||||
pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Connect)
|
||||
}
|
||||
|
||||
/// Creates route handler with `OPTIONS` method guard.
|
||||
///
|
||||
/// Syntax: `#[options("path"[, attributes])]`
|
||||
///
|
||||
/// Attributes are the same as in [options](attr.options.html)
|
||||
#[proc_macro_attribute]
|
||||
pub fn options(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Options)
|
||||
}
|
||||
|
||||
/// Creates route handler with `TRACE` method guard.
|
||||
///
|
||||
/// Syntax: `#[trace("path"[, attributes])]`
|
||||
///
|
||||
/// Attributes are the same as in [trace](attr.trace.html)
|
||||
#[proc_macro_attribute]
|
||||
pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Trace)
|
||||
}
|
||||
|
||||
/// Creates route handler with `PATCH` method guard.
|
||||
///
|
||||
/// Syntax: `#[patch("path"[, attributes])]`
|
||||
///
|
||||
/// Attributes are the same as in [patch](attr.patch.html)
|
||||
#[proc_macro_attribute]
|
||||
pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
route::generate(args, input, route::GuardType::Patch)
|
||||
method_macro! {
|
||||
Get, get,
|
||||
Post, post,
|
||||
Put, put,
|
||||
Delete, delete,
|
||||
Head, head,
|
||||
Connect, connect,
|
||||
Options, options,
|
||||
Trace, trace,
|
||||
Patch, patch,
|
||||
}
|
||||
|
||||
/// Marks async main function as the actix system entry-point.
|
||||
///
|
||||
/// ## Usage
|
||||
/// # Actix Web Re-export
|
||||
/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications.
|
||||
///
|
||||
/// # Usage
|
||||
/// ```rust
|
||||
/// #[actix_web::main]
|
||||
/// #[actix_web_codegen::main]
|
||||
/// async fn main() {
|
||||
/// async { println!("Hello world"); }.await
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
#[cfg(not(test))] // Work around for rust-lang/rust#62127
|
||||
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||
use quote::quote;
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
extern crate proc_macro;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
|
||||
@ -17,53 +20,81 @@ impl ToTokens for ResourceType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum GuardType {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Head,
|
||||
Connect,
|
||||
Options,
|
||||
Trace,
|
||||
Patch,
|
||||
}
|
||||
|
||||
impl GuardType {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
GuardType::Get => "Get",
|
||||
GuardType::Post => "Post",
|
||||
GuardType::Put => "Put",
|
||||
GuardType::Delete => "Delete",
|
||||
GuardType::Head => "Head",
|
||||
GuardType::Connect => "Connect",
|
||||
GuardType::Options => "Options",
|
||||
GuardType::Trace => "Trace",
|
||||
GuardType::Patch => "Patch",
|
||||
macro_rules! method_type {
|
||||
(
|
||||
$($variant:ident, $upper:ident,)+
|
||||
) => {
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum MethodType {
|
||||
$(
|
||||
$variant,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
impl MethodType {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$variant => stringify!($variant),)+
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(method: &str) -> Result<Self, String> {
|
||||
match method {
|
||||
$(stringify!($upper) => Ok(Self::$variant),)+
|
||||
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl ToTokens for GuardType {
|
||||
method_type! {
|
||||
Get, GET,
|
||||
Post, POST,
|
||||
Put, PUT,
|
||||
Delete, DELETE,
|
||||
Head, HEAD,
|
||||
Connect, CONNECT,
|
||||
Options, OPTIONS,
|
||||
Trace, TRACE,
|
||||
Patch, PATCH,
|
||||
}
|
||||
|
||||
impl ToTokens for MethodType {
|
||||
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||
let ident = Ident::new(self.as_str(), Span::call_site());
|
||||
stream.append(ident);
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&syn::LitStr> for MethodType {
|
||||
type Error = syn::Error;
|
||||
|
||||
fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
|
||||
Self::parse(value.value().as_str())
|
||||
.map_err(|message| syn::Error::new_spanned(value, message))
|
||||
}
|
||||
}
|
||||
|
||||
struct Args {
|
||||
path: syn::LitStr,
|
||||
guards: Vec<Ident>,
|
||||
wrappers: Vec<syn::Type>,
|
||||
methods: HashSet<MethodType>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn new(args: AttributeArgs) -> syn::Result<Self> {
|
||||
fn new(args: AttributeArgs, method: Option<MethodType>) -> syn::Result<Self> {
|
||||
let mut path = None;
|
||||
let mut guards = Vec::new();
|
||||
let mut wrappers = Vec::new();
|
||||
let mut methods = HashSet::new();
|
||||
|
||||
let is_route_macro = method.is_none();
|
||||
if let Some(method) = method {
|
||||
methods.insert(method);
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
match arg {
|
||||
NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
|
||||
@ -96,10 +127,33 @@ impl Args {
|
||||
"Attribute wrap expects type",
|
||||
));
|
||||
}
|
||||
} else if nv.path.is_ident("method") {
|
||||
if !is_route_macro {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&nv,
|
||||
"HTTP method forbidden here. To handle multiple methods, use `route` instead",
|
||||
));
|
||||
} else if let syn::Lit::Str(ref lit) = nv.lit {
|
||||
let method = MethodType::try_from(lit)?;
|
||||
if !methods.insert(method) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&nv.lit,
|
||||
&format!(
|
||||
"HTTP method defined more than once: `{}`",
|
||||
lit.value()
|
||||
),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
nv.lit,
|
||||
"Attribute method expects literal string!",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
nv.path,
|
||||
"Unknown attribute key is specified. Allowed: guard and wrap",
|
||||
"Unknown attribute key is specified. Allowed: guard, method and wrap",
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -112,6 +166,7 @@ impl Args {
|
||||
path: path.unwrap(),
|
||||
guards,
|
||||
wrappers,
|
||||
methods,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -121,7 +176,6 @@ pub struct Route {
|
||||
args: Args,
|
||||
ast: syn::ItemFn,
|
||||
resource_type: ResourceType,
|
||||
guard: GuardType,
|
||||
}
|
||||
|
||||
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
|
||||
@ -150,21 +204,30 @@ impl Route {
|
||||
pub fn new(
|
||||
args: AttributeArgs,
|
||||
input: TokenStream,
|
||||
guard: GuardType,
|
||||
method: Option<MethodType>,
|
||||
) -> syn::Result<Self> {
|
||||
if args.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
format!(
|
||||
r#"invalid server definition, expected #[{}("<some path>")]"#,
|
||||
guard.as_str().to_ascii_lowercase()
|
||||
r#"invalid service definition, expected #[{}("<some path>")]"#,
|
||||
method
|
||||
.map(|it| it.as_str())
|
||||
.unwrap_or("route")
|
||||
.to_ascii_lowercase()
|
||||
),
|
||||
));
|
||||
}
|
||||
let ast: syn::ItemFn = syn::parse(input)?;
|
||||
let name = ast.sig.ident.clone();
|
||||
|
||||
let args = Args::new(args)?;
|
||||
let args = Args::new(args, method)?;
|
||||
if args.methods.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
"The #[route(..)] macro requires at least one `method` attribute",
|
||||
));
|
||||
}
|
||||
|
||||
let resource_type = if ast.sig.asyncness.is_some() {
|
||||
ResourceType::Async
|
||||
@ -185,7 +248,6 @@ impl Route {
|
||||
args,
|
||||
ast,
|
||||
resource_type,
|
||||
guard,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -194,17 +256,36 @@ impl ToTokens for Route {
|
||||
fn to_tokens(&self, output: &mut TokenStream2) {
|
||||
let Self {
|
||||
name,
|
||||
guard,
|
||||
ast,
|
||||
args:
|
||||
Args {
|
||||
path,
|
||||
guards,
|
||||
wrappers,
|
||||
methods,
|
||||
},
|
||||
resource_type,
|
||||
} = self;
|
||||
let resource_name = name.to_string();
|
||||
let method_guards = {
|
||||
let mut others = methods.iter();
|
||||
// unwrapping since length is checked to be at least one
|
||||
let first = others.next().unwrap();
|
||||
|
||||
if methods.len() > 1 {
|
||||
quote! {
|
||||
.guard(
|
||||
actix_web::guard::Any(actix_web::guard::#first())
|
||||
#(.or(actix_web::guard::#others()))*
|
||||
)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.guard(actix_web::guard::#first())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let stream = quote! {
|
||||
#[allow(non_camel_case_types, missing_docs)]
|
||||
pub struct #name;
|
||||
@ -214,7 +295,7 @@ impl ToTokens for Route {
|
||||
#ast
|
||||
let __resource = actix_web::Resource::new(#path)
|
||||
.name(#resource_name)
|
||||
.guard(actix_web::guard::#guard())
|
||||
#method_guards
|
||||
#(.guard(actix_web::guard::fn_guard(#guards)))*
|
||||
#(.wrap(#wrappers))*
|
||||
.#resource_type(#name);
|
||||
@ -228,13 +309,13 @@ impl ToTokens for Route {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn generate(
|
||||
pub(crate) fn with_method(
|
||||
method: Option<MethodType>,
|
||||
args: TokenStream,
|
||||
input: TokenStream,
|
||||
guard: GuardType,
|
||||
) -> TokenStream {
|
||||
let args = parse_macro_input!(args as syn::AttributeArgs);
|
||||
match Route::new(args, input, guard) {
|
||||
match Route::new(args, input, method) {
|
||||
Ok(route) => route.into_token_stream().into(),
|
||||
Err(err) => err.to_compile_error().into(),
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ use std::task::{Context, Poll};
|
||||
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
|
||||
use actix_web::http::header::{HeaderName, HeaderValue};
|
||||
use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder};
|
||||
use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, trace};
|
||||
use actix_web_codegen::{
|
||||
connect, delete, get, head, options, patch, post, put, route, trace,
|
||||
};
|
||||
use futures_util::future;
|
||||
|
||||
// Make sure that we can name function as 'config'
|
||||
@ -79,6 +81,11 @@ async fn get_param_test(_: Path<String>) -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
#[route("/multi", method = "GET", method = "POST", method = "HEAD")]
|
||||
async fn route_test() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
pub struct ChangeStatusCode;
|
||||
|
||||
impl<S, B> Transform<S> for ChangeStatusCode
|
||||
@ -172,6 +179,7 @@ async fn test_body() {
|
||||
.service(trace_test)
|
||||
.service(patch_test)
|
||||
.service(test_handler)
|
||||
.service(route_test)
|
||||
});
|
||||
let request = srv.request(http::Method::GET, srv.url("/test"));
|
||||
let response = request.send().await.unwrap();
|
||||
@ -210,6 +218,22 @@ async fn test_body() {
|
||||
let request = srv.request(http::Method::GET, srv.url("/test"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
let request = srv.request(http::Method::GET, srv.url("/multi"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
let request = srv.request(http::Method::POST, srv.url("/multi"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
let request = srv.request(http::Method::HEAD, srv.url("/multi"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
let request = srv.request(http::Method::PATCH, srv.url("/multi"));
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(!response.status().is_success());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
36
actix-web-codegen/tests/trybuild.rs
Normal file
36
actix-web-codegen/tests/trybuild.rs
Normal file
@ -0,0 +1,36 @@
|
||||
#[test]
|
||||
fn compile_macros() {
|
||||
let t = trybuild::TestCases::new();
|
||||
|
||||
t.pass("tests/trybuild/simple.rs");
|
||||
t.compile_fail("tests/trybuild/simple-fail.rs");
|
||||
|
||||
t.pass("tests/trybuild/route-ok.rs");
|
||||
|
||||
test_route_duplicate_unexpected_method(&t);
|
||||
test_route_missing_method(&t)
|
||||
}
|
||||
|
||||
#[rustversion::stable(1.42)]
|
||||
fn test_route_missing_method(t: &trybuild::TestCases) {
|
||||
t.compile_fail("tests/trybuild/route-missing-method-fail-msrv.rs");
|
||||
}
|
||||
|
||||
#[rustversion::not(stable(1.42))]
|
||||
#[rustversion::not(nightly)]
|
||||
fn test_route_missing_method(t: &trybuild::TestCases) {
|
||||
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
|
||||
}
|
||||
|
||||
#[rustversion::nightly]
|
||||
fn test_route_missing_method(_t: &trybuild::TestCases) {}
|
||||
|
||||
// FIXME: Re-test them on nightly once rust-lang/rust#77993 is fixed.
|
||||
#[rustversion::not(nightly)]
|
||||
fn test_route_duplicate_unexpected_method(t: &trybuild::TestCases) {
|
||||
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
|
||||
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
|
||||
}
|
||||
|
||||
#[rustversion::nightly]
|
||||
fn test_route_duplicate_unexpected_method(_t: &trybuild::TestCases) {}
|
@ -0,0 +1,17 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[route("/", method="GET", method="GET")]
|
||||
async fn index() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() {
|
||||
use actix_web::{App, test};
|
||||
|
||||
let srv = test::start(|| App::new().service(index));
|
||||
|
||||
let request = srv.get("/");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
error: HTTP method defined more than once: `GET`
|
||||
--> $DIR/route-duplicate-method-fail.rs:3:35
|
||||
|
|
||||
3 | #[route("/", method="GET", method="GET")]
|
||||
| ^^^^^
|
||||
|
||||
error[E0425]: cannot find value `index` in this scope
|
||||
--> $DIR/route-duplicate-method-fail.rs:12:49
|
||||
|
|
||||
12 | let srv = test::start(|| App::new().service(index));
|
||||
| ^^^^^ not found in this scope
|
@ -0,0 +1 @@
|
||||
route-missing-method-fail.rs
|
@ -0,0 +1,11 @@
|
||||
error: The #[route(..)] macro requires at least one `method` attribute
|
||||
--> $DIR/route-missing-method-fail-msrv.rs:3:1
|
||||
|
|
||||
3 | #[route("/")]
|
||||
| ^^^^^^^^^^^^^
|
||||
|
||||
error[E0425]: cannot find value `index` in this scope
|
||||
--> $DIR/route-missing-method-fail-msrv.rs:12:49
|
||||
|
|
||||
12 | let srv = test::start(|| App::new().service(index));
|
||||
| ^^^^^ not found in this scope
|
@ -0,0 +1,17 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[route("/")]
|
||||
async fn index() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() {
|
||||
use actix_web::{App, test};
|
||||
|
||||
let srv = test::start(|| App::new().service(index));
|
||||
|
||||
let request = srv.get("/");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
error: The #[route(..)] macro requires at least one `method` attribute
|
||||
--> $DIR/route-missing-method-fail.rs:3:1
|
||||
|
|
||||
3 | #[route("/")]
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0425]: cannot find value `index` in this scope
|
||||
--> $DIR/route-missing-method-fail.rs:12:49
|
||||
|
|
||||
12 | let srv = test::start(|| App::new().service(index));
|
||||
| ^^^^^ not found in this scope
|
17
actix-web-codegen/tests/trybuild/route-ok.rs
Normal file
17
actix-web-codegen/tests/trybuild/route-ok.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[route("/", method="GET", method="HEAD")]
|
||||
async fn index() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() {
|
||||
use actix_web::{App, test};
|
||||
|
||||
let srv = test::start(|| App::new().service(index));
|
||||
|
||||
let request = srv.get("/");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[route("/", method="UNEXPECTED")]
|
||||
async fn index() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() {
|
||||
use actix_web::{App, test};
|
||||
|
||||
let srv = test::start(|| App::new().service(index));
|
||||
|
||||
let request = srv.get("/");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
error: Unexpected HTTP method: `UNEXPECTED`
|
||||
--> $DIR/route-unexpected-method-fail.rs:3:21
|
||||
|
|
||||
3 | #[route("/", method="UNEXPECTED")]
|
||||
| ^^^^^^^^^^^^
|
||||
|
||||
error[E0425]: cannot find value `index` in this scope
|
||||
--> $DIR/route-unexpected-method-fail.rs:12:49
|
||||
|
|
||||
12 | let srv = test::start(|| App::new().service(index));
|
||||
| ^^^^^ not found in this scope
|
30
actix-web-codegen/tests/trybuild/simple-fail.rs
Normal file
30
actix-web-codegen/tests/trybuild/simple-fail.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[get("/one", other)]
|
||||
async fn one() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[post(/two)]
|
||||
async fn two() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
static PATCH_PATH: &str = "/three";
|
||||
|
||||
#[patch(PATCH_PATH)]
|
||||
async fn three() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[delete("/four", "/five")]
|
||||
async fn four() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
#[delete("/five", method="GET")]
|
||||
async fn five() -> String {
|
||||
"Hello World!".to_owned()
|
||||
}
|
||||
|
||||
fn main() {}
|
29
actix-web-codegen/tests/trybuild/simple-fail.stderr
Normal file
29
actix-web-codegen/tests/trybuild/simple-fail.stderr
Normal file
@ -0,0 +1,29 @@
|
||||
error: Unknown attribute.
|
||||
--> $DIR/simple-fail.rs:3:15
|
||||
|
|
||||
3 | #[get("/one", other)]
|
||||
| ^^^^^
|
||||
|
||||
error: expected identifier or literal
|
||||
--> $DIR/simple-fail.rs:8:8
|
||||
|
|
||||
8 | #[post(/two)]
|
||||
| ^
|
||||
|
||||
error: Unknown attribute.
|
||||
--> $DIR/simple-fail.rs:15:9
|
||||
|
|
||||
15 | #[patch(PATCH_PATH)]
|
||||
| ^^^^^^^^^^
|
||||
|
||||
error: Multiple paths specified! Should be only one!
|
||||
--> $DIR/simple-fail.rs:20:19
|
||||
|
|
||||
20 | #[delete("/four", "/five")]
|
||||
| ^^^^^^^
|
||||
|
||||
error: HTTP method forbidden here. To handle multiple methods, use `route` instead
|
||||
--> $DIR/simple-fail.rs:25:19
|
||||
|
|
||||
25 | #[delete("/five", method="GET")]
|
||||
| ^^^^^^^^^^^^
|
16
actix-web-codegen/tests/trybuild/simple.rs
Normal file
16
actix-web-codegen/tests/trybuild/simple.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use actix_web::{Responder, HttpResponse, App, test};
|
||||
use actix_web_codegen::*;
|
||||
|
||||
#[get("/config")]
|
||||
async fn config() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() {
|
||||
let srv = test::start(|| App::new().service(config));
|
||||
|
||||
let request = srv.get("/config");
|
||||
let response = request.send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
}
|
@ -3,6 +3,35 @@
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 2.0.1 - 2020-10-30
|
||||
### Changed
|
||||
* Upgrade `base64` to `0.13`. [#1744]
|
||||
* Deprecate `ClientRequest::{if_some, if_true}`. [#1760]
|
||||
|
||||
### Fixed
|
||||
* Use `Accept-Encoding: identity` instead of `Accept-Encoding: br` when no compression feature
|
||||
is enabled [#1737]
|
||||
|
||||
[#1737]: https://github.com/actix/actix-web/pull/1737
|
||||
[#1760]: https://github.com/actix/actix-web/pull/1760
|
||||
[#1744]: https://github.com/actix/actix-web/pull/1744
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
### Changed
|
||||
* `Client::build` was renamed to `Client::builder`.
|
||||
|
||||
|
||||
## 2.0.0-beta.4 - 2020-09-09
|
||||
### Changed
|
||||
* Update actix-codec & actix-tls dependencies.
|
||||
|
||||
|
||||
## 2.0.0-beta.3 - 2020-08-17
|
||||
### Changed
|
||||
* Update `rustls` to 0.18
|
||||
|
||||
|
||||
## 2.0.0-beta.2 - 2020-07-21
|
||||
### Changed
|
||||
* Update `actix-http` dependency to 2.0.0-beta.2
|
||||
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "awc"
|
||||
version = "2.0.0-beta.2"
|
||||
version = "2.0.1"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Async HTTP client library that uses the Actix runtime."
|
||||
description = "Async HTTP and WebSocket client library built on the Actix ecosystem"
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "framework", "async", "web"]
|
||||
homepage = "https://actix.rs"
|
||||
@ -37,13 +37,14 @@ rustls = ["rust-tls", "actix-http/rustls"]
|
||||
compress = ["actix-http/compress"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.2.0"
|
||||
actix-service = "1.0.1"
|
||||
actix-http = "2.0.0-beta.2"
|
||||
actix-codec = "0.3.0"
|
||||
actix-service = "1.0.6"
|
||||
actix-http = "2.0.0"
|
||||
actix-rt = "1.0.0"
|
||||
|
||||
base64 = "0.12"
|
||||
base64 = "0.13"
|
||||
bytes = "0.5.3"
|
||||
cfg-if = "1.0"
|
||||
derive_more = "0.99.2"
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
log =" 0.4"
|
||||
@ -54,16 +55,16 @@ serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.6.1"
|
||||
open-ssl = { version = "0.10", package = "openssl", optional = true }
|
||||
rust-tls = { version = "0.17.0", package = "rustls", optional = true, features = ["dangerous_configuration"] }
|
||||
rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = ["dangerous_configuration"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] }
|
||||
actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] }
|
||||
actix-http = { version = "2.0.0-beta.2", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] }
|
||||
actix-utils = "1.0.3"
|
||||
actix-connect = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-web = { version = "3.0.0", features = ["openssl"] }
|
||||
actix-http = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0", features = ["openssl"] }
|
||||
actix-utils = "2.0.0"
|
||||
actix-server = "1.0.0"
|
||||
actix-tls = { version = "2.0.0-alpha.1", features = ["openssl", "rustls"] }
|
||||
actix-tls = { version = "2.0.0", features = ["openssl", "rustls"] }
|
||||
brotli2 = "0.3.2"
|
||||
flate2 = "1.0.13"
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
|
@ -1,14 +1,19 @@
|
||||
# Actix http client [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/awc) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# awc (Actix Web Client)
|
||||
|
||||
An HTTP Client
|
||||
> Async HTTP and WebSocket client library.
|
||||
|
||||
## Documentation & community resources
|
||||
[](https://crates.io/crates/awc)
|
||||
[](https://docs.rs/awc/2.0.1)
|
||||

|
||||
[](https://deps.rs/crate/awc/2.0.1)
|
||||
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/awc/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [awc](https://crates.io/crates/awc)
|
||||
* Minimum supported Rust version: 1.40 or later
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/awc/2.0.1)
|
||||
- [Example Project](https://github.com/actix/examples/tree/HEAD/awc_https)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum Supported Rust Version (MSRV): 1.42.0
|
||||
|
||||
## Example
|
||||
|
||||
|
@ -152,7 +152,7 @@ where
|
||||
let (head, framed) =
|
||||
connection.open_tunnel(RequestHeadType::from(head)).await?;
|
||||
|
||||
let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io))));
|
||||
let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io))));
|
||||
Ok((head, framed))
|
||||
})
|
||||
}
|
||||
@ -186,7 +186,7 @@ where
|
||||
.open_tunnel(RequestHeadType::Rc(head, extra_headers))
|
||||
.await?;
|
||||
|
||||
let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io))));
|
||||
let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io))));
|
||||
Ok((head, framed))
|
||||
})
|
||||
}
|
||||
|
121
awc/src/lib.rs
121
awc/src/lib.rs
@ -1,27 +1,98 @@
|
||||
#![warn(rust_2018_idioms, warnings)]
|
||||
//! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem.
|
||||
//!
|
||||
//! ## Making a GET request
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[actix_rt::main]
|
||||
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
||||
//! let mut client = awc::Client::default();
|
||||
//! let response = client.get("http://www.rust-lang.org") // <- Create request builder
|
||||
//! .header("User-Agent", "Actix-web")
|
||||
//! .send() // <- Send http request
|
||||
//! .await?;
|
||||
//!
|
||||
//! println!("Response: {:?}", response);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Making POST requests
|
||||
//!
|
||||
//! ### Raw body contents
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[actix_rt::main]
|
||||
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
||||
//! let mut client = awc::Client::default();
|
||||
//! let response = client.post("http://httpbin.org/post")
|
||||
//! .send_body("Raw body contents")
|
||||
//! .await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Forms
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[actix_rt::main]
|
||||
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
||||
//! let params = [("foo", "bar"), ("baz", "quux")];
|
||||
//!
|
||||
//! let mut client = awc::Client::default();
|
||||
//! let response = client.post("http://httpbin.org/post")
|
||||
//! .send_form(¶ms)
|
||||
//! .await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ### JSON
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[actix_rt::main]
|
||||
//! # async fn main() -> Result<(), awc::error::SendRequestError> {
|
||||
//! let request = serde_json::json!({
|
||||
//! "lang": "rust",
|
||||
//! "body": "json"
|
||||
//! });
|
||||
//!
|
||||
//! let mut client = awc::Client::default();
|
||||
//! let response = client.post("http://httpbin.org/post")
|
||||
//! .send_json(&request)
|
||||
//! .await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## WebSocket support
|
||||
//!
|
||||
//! ```
|
||||
//! # #[actix_rt::main]
|
||||
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use futures_util::{sink::SinkExt, stream::StreamExt};
|
||||
//! let (_resp, mut connection) = awc::Client::new()
|
||||
//! .ws("ws://echo.websocket.org")
|
||||
//! .connect()
|
||||
//! .await?;
|
||||
//!
|
||||
//! connection
|
||||
//! .send(awc::ws::Message::Text("Echo".to_string()))
|
||||
//! .await?;
|
||||
//! let response = connection.next().await.unwrap()?;
|
||||
//! # assert_eq!(response, awc::ws::Frame::Text("Echo".as_bytes().into()));
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(
|
||||
clippy::type_complexity,
|
||||
clippy::borrow_interior_mutable_const,
|
||||
clippy::needless_doctest_main
|
||||
)]
|
||||
//! An HTTP Client
|
||||
//!
|
||||
//! ```rust
|
||||
//! use actix_rt::System;
|
||||
//! use awc::Client;
|
||||
//!
|
||||
//! #[actix_rt::main]
|
||||
//! async fn main() {
|
||||
//! let mut client = Client::default();
|
||||
//!
|
||||
//! let response = client.get("http://www.rust-lang.org") // <- Create request builder
|
||||
//! .header("User-Agent", "Actix-web")
|
||||
//! .send() // <- Send http request
|
||||
//! .await;
|
||||
//!
|
||||
//! println!("Response: {:?}", response);
|
||||
//! }
|
||||
//! ```
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::convert::TryFrom;
|
||||
use std::rc::Rc;
|
||||
@ -51,7 +122,9 @@ pub use self::sender::SendClientRequest;
|
||||
|
||||
use self::connect::{Connect, ConnectorWrapper};
|
||||
|
||||
/// An HTTP Client
|
||||
/// An asynchronous HTTP and WebSocket client.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use awc::Client;
|
||||
@ -95,8 +168,9 @@ impl Client {
|
||||
Client::default()
|
||||
}
|
||||
|
||||
/// Build client instance.
|
||||
pub fn build() -> ClientBuilder {
|
||||
/// Create `Client` builder.
|
||||
/// This function is equivalent of `ClientBuilder::new()`.
|
||||
pub fn builder() -> ClientBuilder {
|
||||
ClientBuilder::new()
|
||||
}
|
||||
|
||||
@ -193,7 +267,8 @@ impl Client {
|
||||
self.request(Method::OPTIONS, url)
|
||||
}
|
||||
|
||||
/// Construct WebSockets request.
|
||||
/// Initialize a WebSocket connection.
|
||||
/// Returns a WebSocket connection builder.
|
||||
pub fn ws<U>(&self, url: U) -> ws::WebsocketsRequest
|
||||
where
|
||||
Uri: TryFrom<U>,
|
||||
|
@ -21,10 +21,15 @@ use crate::frozen::FrozenClientRequest;
|
||||
use crate::sender::{PrepForSendingError, RequestSender, SendClientRequest};
|
||||
use crate::ClientConfig;
|
||||
|
||||
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))]
|
||||
const HTTPS_ENCODING: &str = "br, gzip, deflate";
|
||||
#[cfg(not(any(feature = "flate2-zlib", feature = "flate2-rust")))]
|
||||
const HTTPS_ENCODING: &str = "br";
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] {
|
||||
const HTTPS_ENCODING: &str = "br, gzip, deflate";
|
||||
} else if #[cfg(feature = "compress")] {
|
||||
const HTTPS_ENCODING: &str = "br";
|
||||
} else {
|
||||
const HTTPS_ENCODING: &str = "identity";
|
||||
}
|
||||
}
|
||||
|
||||
/// An HTTP Client request builder
|
||||
///
|
||||
@ -349,8 +354,9 @@ impl ClientRequest {
|
||||
self
|
||||
}
|
||||
|
||||
/// This method calls provided closure with builder reference if
|
||||
/// value is `true`.
|
||||
/// This method calls provided closure with builder reference if value is `true`.
|
||||
#[doc(hidden)]
|
||||
#[deprecated = "Use an if statement."]
|
||||
pub fn if_true<F>(self, value: bool, f: F) -> Self
|
||||
where
|
||||
F: FnOnce(ClientRequest) -> ClientRequest,
|
||||
@ -362,8 +368,9 @@ impl ClientRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// This method calls provided closure with builder reference if
|
||||
/// value is `Some`.
|
||||
/// This method calls provided closure with builder reference if value is `Some`.
|
||||
#[doc(hidden)]
|
||||
#[deprecated = "Use an if-let construction."]
|
||||
pub fn if_some<T, F>(self, value: Option<T>, f: F) -> Self
|
||||
where
|
||||
F: FnOnce(T, ClientRequest) -> ClientRequest,
|
||||
@ -596,20 +603,27 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_basics() {
|
||||
let mut req = Client::new()
|
||||
let req = Client::new()
|
||||
.put("/")
|
||||
.version(Version::HTTP_2)
|
||||
.set(header::Date(SystemTime::now().into()))
|
||||
.content_type("plain/text")
|
||||
.if_true(true, |req| req.header(header::SERVER, "awc"))
|
||||
.if_true(false, |req| req.header(header::EXPECT, "awc"))
|
||||
.if_some(Some("server"), |val, req| {
|
||||
req.header(header::USER_AGENT, val)
|
||||
})
|
||||
.if_some(Option::<&str>::None, |_, req| {
|
||||
req.header(header::ALLOW, "1")
|
||||
})
|
||||
.content_length(100);
|
||||
.header(header::SERVER, "awc");
|
||||
|
||||
let req = if let Some(val) = Some("server") {
|
||||
req.header(header::USER_AGENT, val)
|
||||
} else {
|
||||
req
|
||||
};
|
||||
|
||||
let req = if let Some(_val) = Option::<&str>::None {
|
||||
req.header(header::ALLOW, "1")
|
||||
} else {
|
||||
req
|
||||
};
|
||||
|
||||
let mut req = req.content_length(100);
|
||||
|
||||
assert!(req.headers().contains_key(header::CONTENT_TYPE));
|
||||
assert!(req.headers().contains_key(header::DATE));
|
||||
assert!(req.headers().contains_key(header::SERVER));
|
||||
@ -617,13 +631,14 @@ mod tests {
|
||||
assert!(!req.headers().contains_key(header::ALLOW));
|
||||
assert!(!req.headers().contains_key(header::EXPECT));
|
||||
assert_eq!(req.head.version, Version::HTTP_2);
|
||||
|
||||
let _ = req.headers_mut();
|
||||
let _ = req.send_body("");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_client_header() {
|
||||
let req = Client::build()
|
||||
let req = Client::builder()
|
||||
.header(header::CONTENT_TYPE, "111")
|
||||
.finish()
|
||||
.get("/");
|
||||
@ -641,7 +656,7 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_client_header_override() {
|
||||
let req = Client::build()
|
||||
let req = Client::builder()
|
||||
.header(header::CONTENT_TYPE, "111")
|
||||
.finish()
|
||||
.get("/")
|
||||
|
@ -193,11 +193,7 @@ impl RequestSender {
|
||||
}
|
||||
};
|
||||
|
||||
SendClientRequest::new(
|
||||
fut,
|
||||
response_decompress,
|
||||
timeout.or_else(|| config.timeout),
|
||||
)
|
||||
SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout))
|
||||
}
|
||||
|
||||
pub(crate) fn send_json<T: Serialize>(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user