mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-03 17:41:30 +02:00
Compare commits
160 Commits
files-v0.3
...
multipart-
Author | SHA1 | Date | |
---|---|---|---|
c09186a2c0 | |||
d3c476b8c2 | |||
dc23559f23 | |||
6d710629af | |||
85753130d9 | |||
00ba8d5549 | |||
51e9e1500b | |||
a03dbe2dcf | |||
57a3722146 | |||
57da1d3c0f | |||
68117543ea | |||
4f5971d79e | |||
93161df141 | |||
e567873326 | |||
7d632d0b7b | |||
36aee18c64 | |||
007a145988 | |||
2d4a174420 | |||
21f6c9d7a5 | |||
e1683313ec | |||
32de9f8840 | |||
1f202d40e4 | |||
ad608aa64e | |||
a1b00b2cd0 | |||
3beb4cf2da | |||
522c9a5ea6 | |||
102bb8f9ab | |||
20b46cdaf9 | |||
2a2a20c3e7 | |||
093d3a6c59 | |||
8c9ea43e23 | |||
f9fcf56d5c | |||
cbda928a33 | |||
1032f04ded | |||
b373e1370d | |||
404b5a7709 | |||
ecf08d5156 | |||
87655b3028 | |||
3a192400a6 | |||
2a7f2c1d59 | |||
05f104c240 | |||
4dccd092f3 | |||
95ccf1c9bc | |||
6cbf27508a | |||
79de04d862 | |||
a4dbaa8ed1 | |||
c7b4c6edfa | |||
2a5215c1d6 | |||
97f615c245 | |||
1a361273e7 | |||
d7ce648445 | |||
fabc68659b | |||
542db82282 | |||
ae63eb8bb2 | |||
7a3776b770 | |||
ff79c33fd4 | |||
b75a9b7a20 | |||
d0c6ca7671 | |||
24d525d978 | |||
1f70ef155d | |||
7981e0068a | |||
32d59ca904 | |||
ea8bf36104 | |||
0b5b463cfa | |||
fe6ad816cc | |||
e72b787ba7 | |||
efc317d3b0 | |||
31057becca | |||
f1a9b45437 | |||
5af46775b8 | |||
70f4747a23 | |||
2f11ef089b | |||
4100c50c70 | |||
a929209967 | |||
49e945c88f | |||
9b42333fac | |||
e5b86d189c | |||
4bfd5c2781 | |||
9b6a089b36 | |||
ceac97bb8d | |||
61b65aa64a | |||
5468c3c410 | |||
b6385c2b4e | |||
5135c1e3a0 | |||
22b451cf2d | |||
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 | |||
cf1c8abe62 | |||
92b5bcd13f | |||
701bdacfa2 | |||
6dc47c4093 | |||
0ec335a39c | |||
f8d5ad6b53 | |||
43c362779d | |||
971ba3eee1 |
15
.github/ISSUE_TEMPLATE/config.yml
vendored
15
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,15 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Gitter channel (actix-web)
|
||||
- name: GitHub Discussions
|
||||
url: https://github.com/actix/actix-web/discussions
|
||||
about: Actix Web Q&A
|
||||
- name: Gitter chat (actix-web)
|
||||
url: https://gitter.im/actix/actix-web
|
||||
about: Please ask and answer questions about the actix-web here.
|
||||
- name: Gitter channel (actix)
|
||||
about: Actix Web Q&A
|
||||
- name: Gitter chat (actix)
|
||||
url: https://gitter.im/actix/actix
|
||||
about: Please ask and answer questions about the actix here.
|
||||
about: Actix (actor framework) Q&A
|
||||
- name: Actix Discord
|
||||
url: https://discord.gg/NWpN5mmg3x
|
||||
about: Actix developer discussion and community chat
|
||||
|
||||
|
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,6 +1,8 @@
|
||||
## PR Type
|
||||
What kind of change does this PR make?
|
||||
<!-- Thanks for considering contributing actix! -->
|
||||
<!-- Please fill out the following to make our reviews easy. -->
|
||||
|
||||
## PR Type
|
||||
<!-- What kind of change does this PR make? -->
|
||||
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
|
||||
INSERT_PR_TYPE
|
||||
|
||||
@ -13,6 +15,7 @@ Check your PR fulfills the following:
|
||||
- [ ] Tests for the changes have been added / updated.
|
||||
- [ ] Documentation comments have been added / updated.
|
||||
- [ ] A changelog entry has been made for the appropriate packages.
|
||||
- [ ] Format code with the latest stable rustfmt
|
||||
|
||||
|
||||
## Overview
|
||||
|
9
.github/workflows/bench.yml
vendored
9
.github/workflows/bench.yml
vendored
@ -1,13 +1,18 @@
|
||||
name: Benchmark (Linux)
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
check_benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
32
.github/workflows/clippy-fmt.yml
vendored
Normal file
32
.github/workflows/clippy-fmt.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: Clippy and rustfmt Check
|
||||
jobs:
|
||||
clippy_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
override: true
|
||||
- name: Check with rustfmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Check with Clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features --all --tests
|
27
.github/workflows/linux.yml
vendored
27
.github/workflows/linux.yml
vendored
@ -1,6 +1,11 @@
|
||||
name: CI (Linux)
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
@ -8,7 +13,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- 1.41.1 # MSRV
|
||||
- 1.46.0 # MSRV
|
||||
- stable
|
||||
- nightly
|
||||
|
||||
@ -16,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
|
||||
@ -25,6 +30,13 @@ jobs:
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.0.1
|
||||
|
||||
- name: check build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@ -53,12 +65,17 @@ jobs:
|
||||
args: --package=awc --no-default-features --features=rustls -- --nocapture
|
||||
|
||||
- name: Generate coverage file
|
||||
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
|
||||
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
cargo install cargo-tarpaulin --vers "^0.13"
|
||||
cargo tarpaulin --out Xml
|
||||
- name: Upload to Codecov
|
||||
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
|
||||
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: cobertura.xml
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
21
.github/workflows/macos.yml
vendored
21
.github/workflows/macos.yml
vendored
@ -1,6 +1,11 @@
|
||||
name: CI (macOS)
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
@ -15,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
|
||||
@ -24,6 +29,13 @@ jobs:
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.0.1
|
||||
|
||||
- name: check build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@ -37,3 +49,8 @@ jobs:
|
||||
args: --all --all-features --no-fail-fast -- --nocapture
|
||||
--skip=test_h2_content_length
|
||||
--skip=test_reading_deflate_encoding_large_random_rustls
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
14
.github/workflows/upload-doc.yml
vendored
14
.github/workflows/upload-doc.yml
vendored
@ -11,12 +11,12 @@ jobs:
|
||||
if: github.repository == 'actix/actix-web'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
toolchain: nightly-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
@ -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.7.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: target/doc
|
||||
|
21
.github/workflows/windows.yml
vendored
21
.github/workflows/windows.yml
vendored
@ -1,6 +1,11 @@
|
||||
name: CI (Windows)
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
VCPKGRS_DYNAMIC: 1
|
||||
@ -18,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
|
||||
@ -36,6 +41,13 @@ jobs:
|
||||
Get-ChildItem C:\vcpkg\installed\x64-windows\bin
|
||||
Get-ChildItem C:\vcpkg\installed\x64-windows\lib
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.0.1
|
||||
|
||||
- name: check build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@ -57,3 +69,8 @@ jobs:
|
||||
--skip=test_connection_force_close
|
||||
--skip=test_connection_server_close
|
||||
--skip=test_connection_wait_queue_force_close
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
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
|
||||
|
162
CHANGES.md
162
CHANGES.md
@ -1,6 +1,160 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 4.0.0-beta.1 - 2021-01-07
|
||||
### Added
|
||||
* `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and
|
||||
`Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865]
|
||||
|
||||
### Changed
|
||||
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
|
||||
* Bumped `rand` to `0.8`.
|
||||
* Update `rust-tls` to `0.19`. [#1813]
|
||||
* Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852]
|
||||
* The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration
|
||||
guide for implications. [#1875]
|
||||
* Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875]
|
||||
* MSRV is now 1.46.0.
|
||||
|
||||
### Fixed
|
||||
* Added the underlying parse error to `test::read_body_json`'s panic message. [#1812]
|
||||
|
||||
### Removed
|
||||
* Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now
|
||||
exposed directly by the `middleware` module.
|
||||
|
||||
[#1812]: https://github.com/actix/actix-web/pull/1812
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
[#1852]: https://github.com/actix/actix-web/pull/1852
|
||||
[#1865]: https://github.com/actix/actix-web/pull/1865
|
||||
[#1875]: https://github.com/actix/actix-web/pull/1875
|
||||
|
||||
|
||||
## 3.3.2 - 2020-12-01
|
||||
### Fixed
|
||||
* Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762]
|
||||
* Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798]
|
||||
* Increase minimum `socket2` version. [#1803]
|
||||
|
||||
[#1762]: https://github.com/actix/actix-web/pull/1762
|
||||
[#1798]: https://github.com/actix/actix-web/pull/1798
|
||||
[#1803]: https://github.com/actix/actix-web/pull/1803
|
||||
|
||||
|
||||
## 3.3.1 - 2020-11-29
|
||||
* Ensure `actix-http` dependency uses same `serde_urlencoded`.
|
||||
|
||||
|
||||
## 3.3.0 - 2020-11-25
|
||||
### Added
|
||||
* Add `Either<A, B>` extractor helper. [#1788]
|
||||
|
||||
### Changed
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1788]: https://github.com/actix/actix-web/pull/1788
|
||||
|
||||
|
||||
## 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
|
||||
* Memory leak of app data in pooled requests. [#1609]
|
||||
|
||||
[#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
|
||||
@ -109,7 +263,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
|
||||
|
||||
@ -154,7 +308,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
* Make UrlEncodedError::Overflow more informativve
|
||||
* Make UrlEncodedError::Overflow more informative
|
||||
|
||||
* Use actix-testing for testing utils
|
||||
|
||||
@ -172,7 +326,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`
|
||||
|
||||
|
@ -34,10 +34,13 @@ This Code of Conduct applies both within project spaces and in public spaces whe
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fafhrd91@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at robjtede@icloud.com ([@robjtede]) or huyuumi@neet.club ([@JohnTitor]). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
[@robjtede]: https://github.com/robjtede
|
||||
[@JohnTitor]: https://github.com/JohnTitor
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
76
Cargo.toml
76
Cargo.toml
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "3.0.0-beta.1"
|
||||
version = "4.0.0-beta.1"
|
||||
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"
|
||||
@ -34,7 +34,7 @@ members = [
|
||||
"actix-multipart",
|
||||
"actix-web-actors",
|
||||
"actix-web-codegen",
|
||||
"test-server",
|
||||
"actix-http-test",
|
||||
]
|
||||
|
||||
[features]
|
||||
@ -47,10 +47,10 @@ compress = ["actix-http/compress", "awc/compress"]
|
||||
secure-cookies = ["actix-http/secure-cookies"]
|
||||
|
||||
# openssl
|
||||
openssl = ["actix-tls/openssl", "awc/openssl", "open-ssl"]
|
||||
openssl = ["actix-tls/accept", "actix-tls/openssl", "awc/openssl", "open-ssl"]
|
||||
|
||||
# rustls
|
||||
rustls = ["actix-tls/rustls", "awc/rustls", "rust-tls"]
|
||||
rustls = ["actix-tls/accept", "actix-tls/rustls", "awc/rustls", "rust-tls"]
|
||||
|
||||
[[example]]
|
||||
name = "basic"
|
||||
@ -64,47 +64,54 @@ 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-router = "0.2.4"
|
||||
actix-rt = "1.1.1"
|
||||
actix-server = "1.0.0"
|
||||
actix-testing = "1.0.0"
|
||||
actix-codec = "0.4.0-beta.1"
|
||||
actix-macros = "0.1.0"
|
||||
actix-router = "0.2.4"
|
||||
actix-rt = "2.0.0-beta.1"
|
||||
actix-server = "2.0.0-beta.2"
|
||||
actix-service = "2.0.0-beta.2"
|
||||
actix-utils = "3.0.0-beta.1"
|
||||
actix-threadpool = "0.3.1"
|
||||
actix-tls = "2.0.0-alpha.1"
|
||||
actix-tls = { version = "3.0.0-beta.2", default-features = false, optional = true }
|
||||
|
||||
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 = "3.0.0-beta.1"
|
||||
awc = { version = "3.0.0-beta.1", default-features = false }
|
||||
|
||||
bytes = "0.5.3"
|
||||
derive_more = "0.99.2"
|
||||
ahash = "0.6"
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-channel = { version = "0.3.5", default-features = false }
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
fxhash = "0.2.1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
socket2 = "0.3"
|
||||
pin-project = "0.4.17"
|
||||
regex = "1.3"
|
||||
serde = { version = "1.0", features=["derive"] }
|
||||
socket2 = "0.3.16"
|
||||
pin-project = "1.0.0"
|
||||
regex = "1.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.6.1"
|
||||
serde_urlencoded = "0.7"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
url = "2.1"
|
||||
open-ssl = { package = "openssl", version = "0.10", optional = true }
|
||||
rust-tls = { package = "rustls", version = "0.17.0", optional = true }
|
||||
tinyvec = { version = "0.3", features = ["alloc"] }
|
||||
rust-tls = { package = "rustls", version = "0.19.0", optional = true }
|
||||
smallvec = "1.6"
|
||||
|
||||
[dev-dependencies]
|
||||
actix = "0.10.0-alpha.1"
|
||||
rand = "0.7"
|
||||
env_logger = "0.7"
|
||||
actix = "0.11.0-beta.1"
|
||||
actix-http = { version = "3.0.0-beta.1", features = ["actors"] }
|
||||
rand = "0.8"
|
||||
env_logger = "0.8"
|
||||
serde_derive = "1.0"
|
||||
brotli2 = "0.3.2"
|
||||
flate2 = "1.0.13"
|
||||
@ -118,10 +125,11 @@ codegen-units = 1
|
||||
[patch.crates-io]
|
||||
actix-web = { path = "." }
|
||||
actix-http = { path = "actix-http" }
|
||||
actix-http-test = { path = "test-server" }
|
||||
actix-http-test = { path = "actix-http-test" }
|
||||
actix-web-actors = { path = "actix-web-actors" }
|
||||
actix-web-codegen = { path = "actix-web-codegen" }
|
||||
actix-files = { path = "actix-files" }
|
||||
actix-multipart = { path = "actix-multipart" }
|
||||
actix-files = { path = "actix-files" }
|
||||
awc = { path = "awc" }
|
||||
|
||||
[[bench]]
|
||||
|
@ -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
|
||||
|
53
MIGRATION.md
53
MIGRATION.md
@ -1,17 +1,70 @@
|
||||
## Unreleased
|
||||
|
||||
* The default `NormalizePath` behavior now strips trailing slashes by default. This was
|
||||
previously documented to be the case in v3 but the behavior now matches. The effect is that
|
||||
routes defined with trailing slashes will become inaccessible when
|
||||
using `NormalizePath::default()`.
|
||||
|
||||
Before: `#[get("/test/")`
|
||||
After: `#[get("/test")`
|
||||
|
||||
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
|
||||
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* 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.
|
||||
|
||||
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||
`u64` instead of a `usize`.
|
||||
|
||||
* Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use
|
||||
destructuring or `.into_inner()`. For example:
|
||||
|
||||
```rust
|
||||
// Previously:
|
||||
async fn some_route(path: web::Path<(String, String)>) -> String {
|
||||
format!("Hello, {} {}", path.0, path.1)
|
||||
}
|
||||
|
||||
// Now (this also worked before):
|
||||
async fn some_route(path: web::Path<(String, String)>) -> String {
|
||||
let (first_name, last_name) = path.into_inner();
|
||||
format!("Hello, {} {}", first_name, last_name)
|
||||
}
|
||||
// Or (this wasn't previously supported):
|
||||
async fn some_route(web::Path((first_name, last_name)): web::Path<(String, String)>) -> String {
|
||||
format!("Hello, {} {}", first_name, last_name)
|
||||
}
|
||||
```
|
||||
|
||||
* `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
|
||||
|
27
README.md
27
README.md
@ -1,19 +1,21 @@
|
||||
<div align="center">
|
||||
<h1>Actix web</h1>
|
||||
<p>
|
||||
<strong>Actix web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
|
||||
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web)
|
||||
[](https://blog.rust-lang.org/2020/02/27/Rust-1.41.1.html)
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/3.3.2)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
||||

|
||||
[](https://deps.rs/crate/actix-web/3.3.2)
|
||||
<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>
|
||||
@ -32,22 +34,17 @@
|
||||
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||
* Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
|
||||
* Supports [Actix actor framework](https://github.com/actix/actix)
|
||||
* Runs on stable Rust 1.41+
|
||||
* Runs on stable Rust 1.46+
|
||||
|
||||
## 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
|
||||
@ -61,8 +58,8 @@ Code:
|
||||
use actix_web::{get, web, App, HttpServer, Responder};
|
||||
|
||||
#[get("/{id}/{name}/index.html")]
|
||||
async fn index(info: web::Path<(u32, String)>) -> impl Responder {
|
||||
format!("Hello {}! id:{}", info.1, info.0)
|
||||
async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder {
|
||||
format!("Hello {}! id:{}", name, id)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
@ -110,5 +107,5 @@ at your option.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Contribution to the actix-web crate is organized under the terms of the Contributor Covenant, the
|
||||
maintainers of Actix web, promises to intervene to uphold that code of conduct.
|
||||
Contribution to the actix-web repo is organized under the terms of the Contributor Covenant.
|
||||
The Actix team promises to intervene to uphold that code of conduct.
|
||||
|
@ -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,41 @@
|
||||
# Changes
|
||||
|
||||
## [Unreleased] - 2020-xx-xx
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
## [0.3.0-beta.1] - 2020-07-15
|
||||
|
||||
## 0.6.0-beta.1 - 2021-01-07
|
||||
* `HttpRange::parse` now has its own error type.
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
|
||||
|
||||
## 0.5.0 - 2020-12-26
|
||||
* Optionally support hidden files/directories. [#1811]
|
||||
|
||||
[#1811]: https://github.com/actix/actix-web/pull/1811
|
||||
|
||||
|
||||
## 0.4.1 - 2020-11-24
|
||||
* Clarify order of parameters in `Files::new` and improve docs.
|
||||
|
||||
|
||||
## 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 +43,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.6.0-beta.1"
|
||||
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,20 +17,19 @@ 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 = "4.0.0-beta.1", default-features = false }
|
||||
actix-service = "2.0.0-beta.2"
|
||||
bitflags = "1"
|
||||
bytes = "0.5.3"
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
derive_more = "0.99.2"
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
derive_more = "0.99.5"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
v_htmlescape = "0.10"
|
||||
v_htmlescape = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-web = { version = "3.0.0-beta.1", features = ["openssl"] }
|
||||
actix-rt = "2.0.0-beta.1"
|
||||
actix-web = "4.0.0-beta.1"
|
||||
|
@ -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/0.5.0)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.5.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.46 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);
|
||||
}
|
||||
}
|
281
actix-files/src/files.rs
Normal file
281
actix-files/src/files.rs
Normal file
@ -0,0 +1,281 @@
|
||||
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
|
||||
|
||||
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
||||
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>>,
|
||||
hidden_files: bool,
|
||||
}
|
||||
|
||||
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(),
|
||||
hidden_files: self.hidden_files,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Files {
|
||||
/// Create new `Files` instance for a specified base directory.
|
||||
///
|
||||
/// # Argument Order
|
||||
/// The first argument (`mount_path`) is the root URL at which the static files are served.
|
||||
/// For example, `/assets` will serve files at `example.com/assets/...`.
|
||||
///
|
||||
/// The second argument (`serve_from`) is the location on disk at which files are loaded.
|
||||
/// This can be a relative path. For example, `./` would serve files from the current
|
||||
/// working directory.
|
||||
///
|
||||
/// # Implementation Notes
|
||||
/// If the mount path is set as the root path `/`, services registered after this one will
|
||||
/// be inaccessible. Register more specific handlers and services first.
|
||||
///
|
||||
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a
|
||||
/// number of threads equal to 5x the number of available logical CPUs. Pool size can be changed
|
||||
/// by setting ACTIX_THREADPOOL environment variable.
|
||||
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
|
||||
let orig_dir = serve_from.into();
|
||||
let dir = match orig_dir.canonicalize() {
|
||||
Ok(canon_dir) => canon_dir,
|
||||
Err(_) => {
|
||||
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
||||
PathBuf::new()
|
||||
}
|
||||
};
|
||||
|
||||
Files {
|
||||
path: mount_path.to_owned(),
|
||||
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,
|
||||
hidden_files: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, ServiceRequest>,
|
||||
U: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
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
|
||||
}
|
||||
|
||||
/// Enables serving hidden files and directories, allowing a leading dots in url fragments.
|
||||
#[inline]
|
||||
pub fn use_hidden_files(mut self) -> Self {
|
||||
self.hidden_files = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpServiceFactory for Files {
|
||||
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<ServiceRequest> for Files {
|
||||
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(),
|
||||
hidden_files: self.hidden_files,
|
||||
};
|
||||
|
||||
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,43 +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;
|
||||
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, ()>;
|
||||
@ -50,613 +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,
|
||||
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,
|
||||
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 behaviour
|
||||
match *req.method() {
|
||||
Method::HEAD | Method::GET => true,
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -952,9 +388,7 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_content_range_headers() {
|
||||
let srv = test::start(|| {
|
||||
App::new().service(Files::new("/", "."))
|
||||
});
|
||||
let srv = test::start(|| App::new().service(Files::new("/", ".")));
|
||||
|
||||
// Valid range header
|
||||
let response = srv
|
||||
@ -979,9 +413,7 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_content_length_headers() {
|
||||
let srv = test::start(|| {
|
||||
App::new().service(Files::new("/", "."))
|
||||
});
|
||||
let srv = test::start(|| App::new().service(Files::new("/", ".")));
|
||||
|
||||
// Valid range header
|
||||
let response = srv
|
||||
@ -1014,21 +446,15 @@ 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);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_head_content_length_headers() {
|
||||
let srv = test::start(|| {
|
||||
App::new().service(Files::new("/", "."))
|
||||
});
|
||||
let srv = test::start(|| App::new().service(Files::new("/", ".")));
|
||||
|
||||
let response = srv
|
||||
.head("/tests/test.binary")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let response = srv.head("/tests/test.binary").send().await.unwrap();
|
||||
|
||||
let content_length = response
|
||||
.headers()
|
||||
@ -1053,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);
|
||||
}
|
||||
|
||||
@ -1097,12 +523,10 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_content_encoding() {
|
||||
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||
web::resource("/").to(|| {
|
||||
async {
|
||||
NamedFile::open("Cargo.toml")
|
||||
.unwrap()
|
||||
.set_content_encoding(header::ContentEncoding::Identity)
|
||||
}
|
||||
web::resource("/").to(|| async {
|
||||
NamedFile::open("Cargo.toml")
|
||||
.unwrap()
|
||||
.set_content_encoding(header::ContentEncoding::Identity)
|
||||
}),
|
||||
))
|
||||
.await;
|
||||
@ -1119,12 +543,10 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_content_encoding_gzip() {
|
||||
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||
web::resource("/").to(|| {
|
||||
async {
|
||||
NamedFile::open("Cargo.toml")
|
||||
.unwrap()
|
||||
.set_content_encoding(header::ContentEncoding::Gzip)
|
||||
}
|
||||
web::resource("/").to(|| async {
|
||||
NamedFile::open("Cargo.toml")
|
||||
.unwrap()
|
||||
.set_content_encoding(header::ContentEncoding::Gzip)
|
||||
}),
|
||||
))
|
||||
.await;
|
||||
@ -1235,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]
|
||||
@ -1351,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,33 +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 mime;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,12 +93,15 @@ impl NamedFile {
|
||||
};
|
||||
|
||||
let ct = from_path(&path).first_or_octet_stream();
|
||||
let disposition_type = match ct.type_() {
|
||||
|
||||
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")),
|
||||
@ -103,16 +109,19 @@ impl NamedFile {
|
||||
value: filename.into_owned().into_bytes(),
|
||||
}))
|
||||
}
|
||||
|
||||
let cd = ContentDisposition {
|
||||
disposition: disposition_type,
|
||||
parameters: parameters,
|
||||
disposition,
|
||||
parameters,
|
||||
};
|
||||
|
||||
(ct, cd)
|
||||
};
|
||||
|
||||
let md = file.metadata()?;
|
||||
let modified = md.modified().ok();
|
||||
let encoding = None;
|
||||
|
||||
Ok(NamedFile {
|
||||
path,
|
||||
file,
|
||||
@ -184,7 +193,7 @@ impl NamedFile {
|
||||
/// image, and video content types, and `attachment` otherwise, and
|
||||
/// the filename is taken from the path provided in the `open` method
|
||||
/// after converting it to UTF-8 using.
|
||||
/// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy).
|
||||
/// [`std::ffi::OsStr::to_string_lossy`]
|
||||
#[inline]
|
||||
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
||||
self.content_disposition = cd;
|
||||
@ -208,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| {
|
||||
@ -243,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,
|
||||
@ -257,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,
|
||||
@ -277,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) {
|
||||
@ -285,6 +315,7 @@ impl NamedFile {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let last_modified = if self.flags.contains(Flags::LAST_MD) {
|
||||
self.last_modified()
|
||||
} else {
|
||||
@ -299,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,
|
||||
@ -310,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,
|
||||
@ -326,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");
|
||||
|
||||
@ -351,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,
|
||||
@ -415,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 {
|
||||
@ -423,6 +467,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -432,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 {
|
||||
@ -440,8 +486,10 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
119
actix-files/src/path_buf.rs
Normal file
119
actix-files/src/path_buf.rs
Normal file
@ -0,0 +1,119 @@
|
||||
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> {
|
||||
Self::parse_path(path, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl PathBufWrap {
|
||||
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
|
||||
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
||||
let mut buf = PathBuf::new();
|
||||
|
||||
for segment in path.split('/') {
|
||||
if segment == ".." {
|
||||
buf.pop();
|
||||
} else if !hidden_files && 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"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_path() {
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/test/.tt", false).map(|t| t.0),
|
||||
Err(UriSegmentError::BadStart('.'))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/test/.tt", true).unwrap().0,
|
||||
PathBuf::from_iter(vec!["test", ".tt"])
|
||||
);
|
||||
}
|
||||
}
|
@ -1,24 +1,33 @@
|
||||
use derive_more::{Display, Error};
|
||||
|
||||
/// 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;
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
#[display(fmt = "Parse HTTP Range failed")]
|
||||
pub struct ParseRangeErr(#[error(not(source))] ());
|
||||
|
||||
impl HttpRange {
|
||||
/// Parses Range HTTP header string as per RFC 2616.
|
||||
///
|
||||
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
||||
/// `size` is full size of response (file).
|
||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ()> {
|
||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
|
||||
if header.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if !header.starts_with(PREFIX) {
|
||||
return Err(());
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
|
||||
let size_sig = size as i64;
|
||||
@ -31,13 +40,14 @@ impl HttpRange {
|
||||
.map(|ra| {
|
||||
let mut start_end_iter = ra.split('-');
|
||||
|
||||
let start_str = start_end_iter.next().ok_or(())?.trim();
|
||||
let end_str = start_end_iter.next().ok_or(())?.trim();
|
||||
let start_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
|
||||
let end_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
|
||||
|
||||
if start_str.is_empty() {
|
||||
// If no start is specified, end specifies the
|
||||
// range start relative to the end of the file.
|
||||
let mut length: i64 = end_str.parse().map_err(|_| ())?;
|
||||
let mut length: i64 =
|
||||
end_str.parse().map_err(|_| ParseRangeErr(()))?;
|
||||
|
||||
if length > size_sig {
|
||||
length = size_sig;
|
||||
@ -48,10 +58,10 @@ impl HttpRange {
|
||||
length: length as u64,
|
||||
}))
|
||||
} else {
|
||||
let start: i64 = start_str.parse().map_err(|_| ())?;
|
||||
let start: i64 = start_str.parse().map_err(|_| ParseRangeErr(()))?;
|
||||
|
||||
if start < 0 {
|
||||
return Err(());
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
if start >= size_sig {
|
||||
no_overlap = true;
|
||||
@ -62,10 +72,11 @@ impl HttpRange {
|
||||
// If no end is specified, range extends to end of the file.
|
||||
size_sig - start
|
||||
} else {
|
||||
let mut end: i64 = end_str.parse().map_err(|_| ())?;
|
||||
let mut end: i64 =
|
||||
end_str.parse().map_err(|_| ParseRangeErr(()))?;
|
||||
|
||||
if start > end {
|
||||
return Err(());
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
|
||||
if end >= size_sig {
|
||||
@ -86,7 +97,7 @@ impl HttpRange {
|
||||
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
|
||||
|
||||
if no_overlap && ranges.is_empty() {
|
||||
return Err(());
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
@ -330,8 +341,7 @@ mod tests {
|
||||
if expected.is_empty() {
|
||||
continue;
|
||||
} else {
|
||||
assert!(
|
||||
false,
|
||||
panic!(
|
||||
"parse({}, {}) returned error {:?}",
|
||||
header,
|
||||
size,
|
||||
@ -343,28 +353,24 @@ mod tests {
|
||||
let got = res.unwrap();
|
||||
|
||||
if got.len() != expected.len() {
|
||||
assert!(
|
||||
false,
|
||||
panic!(
|
||||
"len(parseRange({}, {})) = {}, want {}",
|
||||
header,
|
||||
size,
|
||||
got.len(),
|
||||
expected.len()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
for i in 0..expected.len() {
|
||||
if got[i].start != expected[i].start {
|
||||
assert!(
|
||||
false,
|
||||
panic!(
|
||||
"parseRange({}, {})[{}].start = {}, want {}",
|
||||
header, size, i, got[i].start, expected[i].start
|
||||
)
|
||||
}
|
||||
if got[i].length != expected[i].length {
|
||||
assert!(
|
||||
false,
|
||||
panic!(
|
||||
"parseRange({}, {})[{}].length = {}, want {}",
|
||||
header, size, i, got[i].length, expected[i].length
|
||||
)
|
||||
|
168
actix-files/src/service.rs
Normal file
168
actix-files/src/service.rs
Normal file
@ -0,0 +1,168 @@
|
||||
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>>,
|
||||
pub(crate) hidden_files: bool,
|
||||
}
|
||||
|
||||
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<ServiceRequest> for FilesService {
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Future = FilesServiceFuture;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
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 PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
|
||||
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.**
|
@ -1,7 +1,28 @@
|
||||
# Changes
|
||||
|
||||
## [2.0.0-alpha.1] - 2020-05-23
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.1 - 2021-01-07
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
|
||||
|
||||
## 2.1.0 - 2020-11-25
|
||||
* Add ability to set address for `TestServer`. [#1645]
|
||||
* Upgrade `base64` to `0.13`.
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1645]: https://github.com/actix/actix-web/pull/1645
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
* Update actix-codec and actix-utils dependencies.
|
||||
|
||||
|
||||
## 2.0.0-alpha.1 - 2020-05-23
|
||||
* Update the `time` dependency to 0.2.7
|
||||
* Update `actix-connect` dependency to 2.0.0-alpha.2
|
||||
* Make `test_server` `async` fn.
|
||||
@ -10,74 +31,56 @@
|
||||
* Update `base64` dependency to 0.12
|
||||
* Update `env_logger` dependency to 0.7
|
||||
|
||||
## [1.0.0] - 2019-12-13
|
||||
|
||||
### Changed
|
||||
|
||||
## 1.0.0 - 2019-12-13
|
||||
* Replaced `TestServer::start()` with `test_server()`
|
||||
|
||||
|
||||
## [1.0.0-alpha.3] - 2019-12-07
|
||||
|
||||
### Changed
|
||||
|
||||
## 1.0.0-alpha.3 - 2019-12-07
|
||||
* Migrate to `std::future`
|
||||
|
||||
|
||||
## [0.2.5] - 2019-09-17
|
||||
|
||||
### Changed
|
||||
|
||||
## 0.2.5 - 2019-09-17
|
||||
* Update serde_urlencoded to "0.6.1"
|
||||
* Increase TestServerRuntime timeouts from 500ms to 3000ms
|
||||
|
||||
### Fixed
|
||||
|
||||
* Do not override current `System`
|
||||
|
||||
|
||||
## [0.2.4] - 2019-07-18
|
||||
|
||||
## 0.2.4 - 2019-07-18
|
||||
* Update actix-server to 0.6
|
||||
|
||||
## [0.2.3] - 2019-07-16
|
||||
|
||||
## 0.2.3 - 2019-07-16
|
||||
* Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
||||
|
||||
## [0.2.2] - 2019-06-16
|
||||
|
||||
## 0.2.2 - 2019-06-16
|
||||
* Add .put() and .sput() methods
|
||||
|
||||
## [0.2.1] - 2019-06-05
|
||||
|
||||
## 0.2.1 - 2019-06-05
|
||||
* Add license files
|
||||
|
||||
## [0.2.0] - 2019-05-12
|
||||
|
||||
## 0.2.0 - 2019-05-12
|
||||
* Update awc and actix-http deps
|
||||
|
||||
## [0.1.1] - 2019-04-24
|
||||
|
||||
## 0.1.1 - 2019-04-24
|
||||
* Always make new connection for http client
|
||||
|
||||
|
||||
## [0.1.0] - 2019-04-16
|
||||
|
||||
## 0.1.0 - 2019-04-16
|
||||
* No changes
|
||||
|
||||
|
||||
## [0.1.0-alpha.3] - 2019-04-02
|
||||
|
||||
## 0.1.0-alpha.3 - 2019-04-02
|
||||
* Request functions accept path #743
|
||||
|
||||
|
||||
## [0.1.0-alpha.2] - 2019-03-29
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-03-29
|
||||
* Added TestServerRuntime::load_body() method
|
||||
|
||||
* Update actix-http and awc libraries
|
||||
|
||||
|
||||
## [0.1.0-alpha.1] - 2019-03-28
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
* Initial impl
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "2.0.0-alpha.1"
|
||||
version = "3.0.0-beta.1"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix HTTP test server"
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
readme = "README.md"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
@ -29,28 +29,27 @@ default = []
|
||||
openssl = ["open-ssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "1.0.1"
|
||||
actix-codec = "0.2.0"
|
||||
actix-connect = "2.0.0-alpha.2"
|
||||
actix-utils = "1.0.3"
|
||||
actix-rt = "1.0.0"
|
||||
actix-server = "1.0.0"
|
||||
actix-testing = "1.0.0"
|
||||
awc = "2.0.0-alpha.2"
|
||||
actix-service = "2.0.0-beta.2"
|
||||
actix-codec = "0.4.0-beta.1"
|
||||
actix-tls = "3.0.0-beta.2"
|
||||
actix-utils = "3.0.0-beta.1"
|
||||
actix-rt = "2.0.0-beta.1"
|
||||
actix-server = "2.0.0-beta.2"
|
||||
awc = "3.0.0-beta.1"
|
||||
|
||||
base64 = "0.12"
|
||||
bytes = "0.5.3"
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
http = "0.2.0"
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
http = "0.2.2"
|
||||
log = "0.4"
|
||||
socket2 = "0.3"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.6.1"
|
||||
serde_urlencoded = "0.7"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
open-ssl = { version = "0.10", package = "openssl", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = "3.0.0-alpha.3"
|
||||
actix-http = "2.0.0-beta.1"
|
||||
actix-web = "4.0.0-beta.1"
|
||||
actix-http = "3.0.0-beta.1"
|
15
actix-http-test/README.md
Normal file
15
actix-http-test/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# actix-http-test
|
||||
|
||||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/2.1.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-http-test/2.1.0)
|
||||
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http-test)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum Supported Rust Version (MSRV): 1.46.0
|
@ -1,4 +1,9 @@
|
||||
//! Various helpers for Actix applications to use during testing.
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::{net, thread, time};
|
||||
|
||||
@ -11,8 +16,6 @@ use futures_core::stream::Stream;
|
||||
use http::Method;
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
|
||||
pub use actix_testing::*;
|
||||
|
||||
/// Start test server
|
||||
///
|
||||
/// `TestServer` is very simple test server that simplify process of writing
|
||||
@ -44,21 +47,32 @@ pub use actix_testing::*;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
test_server_with_addr(tcp, factory).await
|
||||
}
|
||||
|
||||
/// Start [`test server`](test_server()) on a concrete Address
|
||||
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
tcp: net::TcpListener,
|
||||
factory: F,
|
||||
) -> TestServer {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
// run server in separate thread
|
||||
thread::spawn(move || {
|
||||
let sys = System::new("actix-test-server");
|
||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let local_addr = tcp.local_addr().unwrap();
|
||||
|
||||
Server::build()
|
||||
let srv = Server::build()
|
||||
.listen("test", tcp, factory)?
|
||||
.workers(1)
|
||||
.disable_signals()
|
||||
.start();
|
||||
.disable_signals();
|
||||
|
||||
sys.block_on(async {
|
||||
srv.start();
|
||||
tx.send((System::current(), local_addr)).unwrap();
|
||||
});
|
||||
|
||||
tx.send((System::current(), local_addr)).unwrap();
|
||||
sys.run()
|
||||
});
|
||||
|
||||
@ -90,9 +104,9 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
|
||||
}
|
||||
};
|
||||
|
||||
Client::build().connector(connector).finish()
|
||||
Client::builder().connector(connector).finish()
|
||||
};
|
||||
actix_connect::start_default_resolver().await.unwrap();
|
||||
actix_tls::connect::start_default_resolver().await.unwrap();
|
||||
|
||||
TestServer {
|
||||
addr,
|
@ -1,6 +1,95 @@
|
||||
# Changes
|
||||
|
||||
## [Unreleased] - xxx
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.1 - 2021-01-07
|
||||
### Added
|
||||
* Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`.
|
||||
|
||||
### Changed
|
||||
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
|
||||
* Bumped `rand` to `0.8`.
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
* Update `h2` to `0.3`. [#1813]
|
||||
* The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864]
|
||||
|
||||
### Removed
|
||||
* Deprecated `on_connect` methods have been removed. Prefer the new
|
||||
`on_connect_ext` technique. [#1857]
|
||||
* Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
||||
due to deprecate of resolver actor. [#1813]
|
||||
* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
||||
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
||||
error would return as `ConnectError::SslError`. [#1813]
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
[#1857]: https://github.com/actix/actix-web/pull/1857
|
||||
[#1864]: https://github.com/actix/actix-web/pull/1864
|
||||
|
||||
|
||||
## 2.2.0 - 2020-11-25
|
||||
### Added
|
||||
* HttpResponse builders for 1xx status codes. [#1768]
|
||||
* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
|
||||
* `TryFrom<u16>` and `TryFrom<f32>` for `http::header::Quality`. [#1797]
|
||||
|
||||
### Fixed
|
||||
* Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
|
||||
|
||||
### Changed
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1767]: https://github.com/actix/actix-web/pull/1767
|
||||
[#1768]: https://github.com/actix/actix-web/pull/1768
|
||||
[#1793]: https://github.com/actix/actix-web/pull/1793
|
||||
[#1797]: https://github.com/actix/actix-web/pull/1797
|
||||
|
||||
|
||||
## 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]
|
||||
|
||||
### Changed
|
||||
* Fix illegal chunked encoding. [#1615]
|
||||
|
||||
[#1614]: https://github.com/actix/actix-web/pull/1614
|
||||
[#1615]: https://github.com/actix/actix-web/pull/1615
|
||||
|
||||
|
||||
## [2.0.0-beta.1] - 2020-07-11
|
||||
|
||||
|
@ -1,46 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fafhrd91@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "2.0.0-beta.1"
|
||||
version = "3.0.0-beta.1"
|
||||
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"
|
||||
@ -25,10 +25,10 @@ path = "src/lib.rs"
|
||||
default = []
|
||||
|
||||
# openssl
|
||||
openssl = ["actix-tls/openssl", "actix-connect/openssl"]
|
||||
openssl = ["actix-tls/openssl"]
|
||||
|
||||
# rustls support
|
||||
rustls = ["actix-tls/rustls", "actix-connect/rustls"]
|
||||
rustls = ["actix-tls/rustls"]
|
||||
|
||||
# enable compressison support
|
||||
compress = ["flate2", "brotli2"]
|
||||
@ -40,29 +40,29 @@ 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-rt = "1.0.0"
|
||||
actix-service = "2.0.0-beta.2"
|
||||
actix-codec = "0.4.0-beta.1"
|
||||
actix-utils = "3.0.0-beta.1"
|
||||
actix-rt = "2.0.0-beta.1"
|
||||
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 = "3.0.0-beta.2"
|
||||
actix = { version = "0.11.0-beta.1", optional = true }
|
||||
|
||||
base64 = "0.12"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.2"
|
||||
bytes = "0.5.3"
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
cookie = { version = "0.14.1", features = ["percent-encode"] }
|
||||
copyless = "0.1.4"
|
||||
derive_more = "0.99.2"
|
||||
derive_more = "0.99.5"
|
||||
either = "1.5.3"
|
||||
encoding_rs = "0.8"
|
||||
futures-channel = { version = "0.3.5", default-features = false }
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
futures-channel = { version = "0.3.7", default-features = false }
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["sink"] }
|
||||
fxhash = "0.2.1"
|
||||
h2 = "0.2.1"
|
||||
http = "0.2.0"
|
||||
h2 = "0.3.0"
|
||||
http = "0.2.2"
|
||||
httparse = "1.3"
|
||||
indexmap = "1.3"
|
||||
itoa = "0.4"
|
||||
@ -71,14 +71,14 @@ language-tags = "0.2"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
pin-project = "0.4.17"
|
||||
rand = "0.7"
|
||||
pin-project = "1.0.0"
|
||||
rand = "0.8"
|
||||
regex = "1.3"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
sha-1 = "0.9"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.6.1"
|
||||
serde_urlencoded = "0.7"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
|
||||
# compression
|
||||
@ -86,20 +86,23 @@ brotli2 = { version="0.3.2", optional = true }
|
||||
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-server = "2.0.0-beta.2"
|
||||
actix-http-test = { version = "3.0.0-beta.1", features = ["openssl"] }
|
||||
actix-tls = { version = "3.0.0-beta.2", 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.19", package = "rustls" }
|
||||
|
||||
[[bench]]
|
||||
name = "content-length"
|
||||
name = "write-camel-case"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "status-line"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "uninit-headers"
|
||||
harness = false
|
||||
|
@ -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.2.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-http/2.2.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)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum Supported Rust Version (MSRV): 1.46.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;
|
||||
|
||||
|
@ -1,291 +0,0 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
use bytes::BytesMut;
|
||||
|
||||
// benchmark sending all requests at the same time
|
||||
fn bench_write_content_length(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_content_length");
|
||||
|
||||
let sizes = [
|
||||
0, 1, 11, 83, 101, 653, 1001, 6323, 10001, 56329, 100001, 123456, 98724245,
|
||||
4294967202,
|
||||
];
|
||||
|
||||
for i in sizes.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_original::write_content_length(i, &mut b)
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_new::write_content_length(i, &mut b)
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("itoa", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_itoa::write_content_length(i, &mut b)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_write_content_length);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _itoa {
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
pub fn write_content_length(n: usize, bytes: &mut BytesMut) {
|
||||
if n == 0 {
|
||||
bytes.put_slice(b"\r\ncontent-length: 0\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut buf = itoa::Buffer::new();
|
||||
|
||||
bytes.put_slice(b"\r\ncontent-length: ");
|
||||
bytes.put_slice(buf.format(n).as_bytes());
|
||||
bytes.put_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
mod _new {
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
const DIGITS_START: u8 = b'0';
|
||||
|
||||
/// NOTE: bytes object has to contain enough space
|
||||
pub fn write_content_length(n: usize, bytes: &mut BytesMut) {
|
||||
if n == 0 {
|
||||
bytes.put_slice(b"\r\ncontent-length: 0\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
bytes.put_slice(b"\r\ncontent-length: ");
|
||||
|
||||
if n < 10 {
|
||||
bytes.put_u8(DIGITS_START + (n as u8));
|
||||
} else if n < 100 {
|
||||
let n = n as u8;
|
||||
|
||||
let d10 = n / 10;
|
||||
let d1 = n % 10;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else if n < 1000 {
|
||||
let n = n as u16;
|
||||
|
||||
let d100 = (n / 100) as u8;
|
||||
let d10 = ((n / 10) % 10) as u8;
|
||||
let d1 = (n % 10) as u8;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d100);
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else if n < 10_000 {
|
||||
let n = n as u16;
|
||||
|
||||
let d1000 = (n / 1000) as u8;
|
||||
let d100 = ((n / 100) % 10) as u8;
|
||||
let d10 = ((n / 10) % 10) as u8;
|
||||
let d1 = (n % 10) as u8;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d1000);
|
||||
bytes.put_u8(DIGITS_START + d100);
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else if n < 100_000 {
|
||||
let n = n as u32;
|
||||
|
||||
let d10000 = (n / 10000) as u8;
|
||||
let d1000 = ((n / 1000) % 10) as u8;
|
||||
let d100 = ((n / 100) % 10) as u8;
|
||||
let d10 = ((n / 10) % 10) as u8;
|
||||
let d1 = (n % 10) as u8;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d10000);
|
||||
bytes.put_u8(DIGITS_START + d1000);
|
||||
bytes.put_u8(DIGITS_START + d100);
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else if n < 1_000_000 {
|
||||
let n = n as u32;
|
||||
|
||||
let d100000 = (n / 100000) as u8;
|
||||
let d10000 = ((n / 10000) % 10) as u8;
|
||||
let d1000 = ((n / 1000) % 10) as u8;
|
||||
let d100 = ((n / 100) % 10) as u8;
|
||||
let d10 = ((n / 10) % 10) as u8;
|
||||
let d1 = (n % 10) as u8;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d100000);
|
||||
bytes.put_u8(DIGITS_START + d10000);
|
||||
bytes.put_u8(DIGITS_START + d1000);
|
||||
bytes.put_u8(DIGITS_START + d100);
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else {
|
||||
write_usize(n, bytes);
|
||||
}
|
||||
|
||||
bytes.put_slice(b"\r\n");
|
||||
}
|
||||
|
||||
fn write_usize(n: usize, bytes: &mut BytesMut) {
|
||||
let mut n = n;
|
||||
|
||||
// 20 chars is max length of a usize (2^64)
|
||||
// digits will be added to the buffer from lsd to msd
|
||||
let mut buf = BytesMut::with_capacity(20);
|
||||
|
||||
while n > 9 {
|
||||
// "pop" the least-significant digit
|
||||
let lsd = (n % 10) as u8;
|
||||
|
||||
// remove the lsd from n
|
||||
n = n / 10;
|
||||
|
||||
buf.put_u8(DIGITS_START + lsd);
|
||||
}
|
||||
|
||||
// put msd to result buffer
|
||||
bytes.put_u8(DIGITS_START + (n as u8));
|
||||
|
||||
// put, in reverse (msd to lsd), remaining digits to buffer
|
||||
for i in (0..buf.len()).rev() {
|
||||
bytes.put_u8(buf[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
use std::{mem, ptr, slice};
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
|
||||
2021222324252627282930313233343536373839\
|
||||
4041424344454647484950515253545556575859\
|
||||
6061626364656667686970717273747576777879\
|
||||
8081828384858687888990919293949596979899";
|
||||
|
||||
/// NOTE: bytes object has to contain enough space
|
||||
pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
|
||||
if n < 10 {
|
||||
let mut buf: [u8; 21] = [
|
||||
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
|
||||
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n',
|
||||
];
|
||||
buf[18] = (n as u8) + b'0';
|
||||
bytes.put_slice(&buf);
|
||||
} else if n < 100 {
|
||||
let mut buf: [u8; 22] = [
|
||||
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
|
||||
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n',
|
||||
];
|
||||
let d1 = n << 1;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
DEC_DIGITS_LUT.as_ptr().add(d1),
|
||||
buf.as_mut_ptr().offset(18),
|
||||
2,
|
||||
);
|
||||
}
|
||||
bytes.put_slice(&buf);
|
||||
} else if n < 1000 {
|
||||
let mut buf: [u8; 23] = [
|
||||
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
|
||||
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r',
|
||||
b'\n',
|
||||
];
|
||||
// decode 2 more chars, if > 2 chars
|
||||
let d1 = (n % 100) << 1;
|
||||
n /= 100;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
DEC_DIGITS_LUT.as_ptr().add(d1),
|
||||
buf.as_mut_ptr().offset(19),
|
||||
2,
|
||||
)
|
||||
};
|
||||
|
||||
// decode last 1
|
||||
buf[18] = (n as u8) + b'0';
|
||||
|
||||
bytes.put_slice(&buf);
|
||||
} else {
|
||||
bytes.put_slice(b"\r\ncontent-length: ");
|
||||
convert_usize(n, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) {
|
||||
let mut curr: isize = 39;
|
||||
let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() };
|
||||
buf[39] = b'\r';
|
||||
buf[40] = b'\n';
|
||||
let buf_ptr = buf.as_mut_ptr();
|
||||
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
|
||||
|
||||
// eagerly decode 4 characters at a time
|
||||
while n >= 10_000 {
|
||||
let rem = (n % 10_000) as isize;
|
||||
n /= 10_000;
|
||||
|
||||
let d1 = (rem / 100) << 1;
|
||||
let d2 = (rem % 100) << 1;
|
||||
curr -= 4;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
|
||||
ptr::copy_nonoverlapping(
|
||||
lut_ptr.offset(d2),
|
||||
buf_ptr.offset(curr + 2),
|
||||
2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// if we reach here numbers are <= 9999, so at most 4 chars long
|
||||
let mut n = n as isize; // possibly reduce 64bit math
|
||||
|
||||
// decode 2 more chars, if > 2 chars
|
||||
if n >= 100 {
|
||||
let d1 = (n % 100) << 1;
|
||||
n /= 100;
|
||||
curr -= 2;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
}
|
||||
|
||||
// decode last 1 or 2 chars
|
||||
if n < 10 {
|
||||
curr -= 1;
|
||||
unsafe {
|
||||
*buf_ptr.offset(curr) = (n as u8) + b'0';
|
||||
}
|
||||
} else {
|
||||
let d1 = n << 1;
|
||||
curr -= 2;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
bytes.extend_from_slice(slice::from_raw_parts(
|
||||
buf_ptr.offset(curr),
|
||||
41 - curr as usize,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
@ -176,7 +176,7 @@ mod _original {
|
||||
buf[5] = b'0';
|
||||
buf[7] = b'9';
|
||||
}
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut curr: isize = 12;
|
||||
|
137
actix-http/benches/uninit-headers.rs
Normal file
137
actix-http/benches/uninit-headers.rs
Normal file
@ -0,0 +1,137 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
use bytes::BytesMut;
|
||||
|
||||
// A Miri run detects UB, seen on this playground:
|
||||
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915
|
||||
|
||||
fn bench_header_parsing(c: &mut Criterion) {
|
||||
c.bench_function("Original (Unsound) [short]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ_SHORT);
|
||||
_original::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("New (safe) [short]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ_SHORT);
|
||||
_new::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("Original (Unsound) [realistic]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ);
|
||||
_original::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("New (safe) [realistic]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ);
|
||||
_new::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_header_parsing);
|
||||
criterion_main!(benches);
|
||||
|
||||
const MAX_HEADERS: usize = 96;
|
||||
|
||||
const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
||||
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct HeaderIndex {
|
||||
name: (usize, usize),
|
||||
value: (usize, usize),
|
||||
}
|
||||
|
||||
const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
||||
name: (0, 0),
|
||||
value: (0, 0),
|
||||
};
|
||||
|
||||
const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||
|
||||
impl HeaderIndex {
|
||||
fn record(
|
||||
bytes: &[u8],
|
||||
headers: &[httparse::Header<'_>],
|
||||
indices: &mut [HeaderIndex],
|
||||
) {
|
||||
let bytes_ptr = bytes.as_ptr() as usize;
|
||||
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
|
||||
let name_start = header.name.as_ptr() as usize - bytes_ptr;
|
||||
let name_end = name_start + header.name.len();
|
||||
indices.name = (name_start, name_end);
|
||||
let value_start = header.value.as_ptr() as usize - bytes_ptr;
|
||||
let value_end = value_start + header.value.len();
|
||||
indices.value = (value_start, value_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test cases taken from:
|
||||
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
|
||||
|
||||
const REQ_SHORT: &'static [u8] = b"\
|
||||
GET / HTTP/1.0\r\n\
|
||||
Host: example.com\r\n\
|
||||
Cookie: session=60; user_id=1\r\n\r\n";
|
||||
|
||||
const REQ: &'static [u8] = b"\
|
||||
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
|
||||
Host: www.kittyhell.com\r\n\
|
||||
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\
|
||||
Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\
|
||||
Accept-Encoding: gzip,deflate\r\n\
|
||||
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\
|
||||
Keep-Alive: 115\r\n\
|
||||
Connection: keep-alive\r\n\
|
||||
Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n";
|
||||
|
||||
mod _new {
|
||||
use super::*;
|
||||
|
||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||
|
||||
let mut req = httparse::Request::new(&mut parsed);
|
||||
match req.parse(src).unwrap() {
|
||||
httparse::Status::Complete(_len) => {
|
||||
HeaderIndex::record(src, req.headers, &mut headers);
|
||||
req.headers.len()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
use super::*;
|
||||
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
let mut req = httparse::Request::new(&mut parsed);
|
||||
match req.parse(src).unwrap() {
|
||||
httparse::Status::Complete(_len) => {
|
||||
HeaderIndex::record(src, req.headers, &mut headers);
|
||||
req.headers.len()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
89
actix-http/benches/write-camel-case.rs
Normal file
89
actix-http/benches/write-camel-case.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
fn bench_write_camel_case(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_camel_case");
|
||||
|
||||
let names = ["connection", "Transfer-Encoding", "transfer-encoding"];
|
||||
|
||||
for &i in &names {
|
||||
let bts = i.as_bytes();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Original", i), bts, |b, bts| {
|
||||
b.iter(|| {
|
||||
let mut buf = black_box([0; 24]);
|
||||
_original::write_camel_case(black_box(bts), &mut buf)
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
|
||||
b.iter(|| {
|
||||
let mut buf = black_box([0; 24]);
|
||||
_new::write_camel_case(black_box(bts), &mut buf)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_write_camel_case);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _new {
|
||||
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
||||
// first copy entire (potentially wrong) slice to output
|
||||
buffer[..value.len()].copy_from_slice(value);
|
||||
|
||||
let mut iter = value.iter();
|
||||
|
||||
// first character should be uppercase
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[0] = c & 0b1101_1111;
|
||||
}
|
||||
|
||||
// track 1 ahead of the current position since that's the location being assigned to
|
||||
let mut index = 2;
|
||||
|
||||
// remaining characters after hyphens should also be uppercase
|
||||
while let Some(&c) = iter.next() {
|
||||
if c == b'-' {
|
||||
// advance iter by one and uppercase if needed
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[index] = c & 0b1101_1111;
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
||||
let mut index = 0;
|
||||
let key = value;
|
||||
let mut key_iter = key.iter();
|
||||
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
while let Some(c) = key_iter.next() {
|
||||
buffer[index] = *c;
|
||||
index += 1;
|
||||
if *c == b'-' {
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{fmt, mem};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::Stream;
|
||||
use futures_util::ready;
|
||||
use futures_core::{ready, Stream};
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::error::Error;
|
||||
@ -21,12 +19,7 @@ pub enum BodySize {
|
||||
|
||||
impl BodySize {
|
||||
pub fn is_eof(&self) -> bool {
|
||||
match self {
|
||||
BodySize::None
|
||||
| BodySize::Empty
|
||||
| BodySize::Sized(0) => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +66,7 @@ impl<T: MessageBody + Unpin> MessageBody for Box<T> {
|
||||
#[pin_project(project = ResponseBodyProj)]
|
||||
pub enum ResponseBody<B> {
|
||||
Body(#[pin] B),
|
||||
Other(#[pin] Body),
|
||||
Other(Body),
|
||||
}
|
||||
|
||||
impl ResponseBody<Body> {
|
||||
@ -115,7 +108,7 @@ impl<B: MessageBody> MessageBody for ResponseBody<B> {
|
||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||
match self.project() {
|
||||
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
||||
ResponseBodyProj::Other(body) => body.poll_next(cx),
|
||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -129,12 +122,11 @@ impl<B: MessageBody> Stream for ResponseBody<B> {
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
match self.project() {
|
||||
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
||||
ResponseBodyProj::Other(body) => body.poll_next(cx),
|
||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project(project = BodyProj)]
|
||||
/// Represents various types of http message body.
|
||||
pub enum Body {
|
||||
/// Empty response. `Content-Length` header is not set.
|
||||
@ -173,10 +165,10 @@ impl MessageBody for Body {
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||
match self.project() {
|
||||
BodyProj::None => Poll::Ready(None),
|
||||
BodyProj::Empty => Poll::Ready(None),
|
||||
BodyProj::Bytes(ref mut bin) => {
|
||||
match self.get_mut() {
|
||||
Body::None => Poll::Ready(None),
|
||||
Body::Empty => Poll::Ready(None),
|
||||
Body::Bytes(ref mut bin) => {
|
||||
let len = bin.len();
|
||||
if len == 0 {
|
||||
Poll::Ready(None)
|
||||
@ -184,7 +176,7 @@ impl MessageBody for Body {
|
||||
Poll::Ready(Some(Ok(mem::take(bin))))
|
||||
}
|
||||
}
|
||||
BodyProj::Message(ref mut body) => Pin::new(body.as_mut()).poll_next(cx),
|
||||
Body::Message(body) => Pin::new(&mut **body).poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -192,14 +184,8 @@ impl MessageBody for Body {
|
||||
impl PartialEq for Body {
|
||||
fn eq(&self, other: &Body) -> bool {
|
||||
match *self {
|
||||
Body::None => match *other {
|
||||
Body::None => true,
|
||||
_ => false,
|
||||
},
|
||||
Body::Empty => match *other {
|
||||
Body::Empty => true,
|
||||
_ => false,
|
||||
},
|
||||
Body::None => matches!(*other, Body::None),
|
||||
Body::Empty => matches!(*other, Body::Empty),
|
||||
Body::Bytes(ref b) => match *other {
|
||||
Body::Bytes(ref b2) => b == b2,
|
||||
_ => false,
|
||||
@ -277,12 +263,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> From<BodyStream<S, E>> for Body
|
||||
impl<S, E> From<BodyStream<S>> for Body
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
||||
E: Into<Error> + 'static,
|
||||
{
|
||||
fn from(s: BodyStream<S, E>) -> Body {
|
||||
fn from(s: BodyStream<S>) -> Body {
|
||||
Body::from_message(s)
|
||||
}
|
||||
}
|
||||
@ -378,27 +364,21 @@ impl MessageBody for String {
|
||||
|
||||
/// Type represent streaming body.
|
||||
/// Response does not contain `content-length` header and appropriate transfer encoding is used.
|
||||
#[pin_project]
|
||||
pub struct BodyStream<S: Unpin, E> {
|
||||
#[pin]
|
||||
pub struct BodyStream<S: Unpin> {
|
||||
stream: S,
|
||||
_t: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<S, E> BodyStream<S, E>
|
||||
impl<S, E> BodyStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||
E: Into<Error>,
|
||||
{
|
||||
pub fn new(stream: S) -> Self {
|
||||
BodyStream {
|
||||
stream,
|
||||
_t: PhantomData,
|
||||
}
|
||||
BodyStream { stream }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> MessageBody for BodyStream<S, E>
|
||||
impl<S, E> MessageBody for BodyStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||
E: Into<Error>,
|
||||
@ -413,13 +393,12 @@ where
|
||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||
/// [`Stream`] ends.
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||
let mut stream = self.project().stream;
|
||||
loop {
|
||||
let stream = stream.as_mut();
|
||||
return Poll::Ready(match ready!(stream.poll_next(cx)) {
|
||||
let stream = &mut self.as_mut().stream;
|
||||
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
|
||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||
opt => opt.map(|res| res.map_err(Into::into)),
|
||||
});
|
||||
@ -429,10 +408,8 @@ where
|
||||
|
||||
/// Type represent streaming body. This body implementation should be used
|
||||
/// if total size of stream is known. Data get sent as is without using transfer encoding.
|
||||
#[pin_project]
|
||||
pub struct SizedStream<S: Unpin> {
|
||||
size: u64,
|
||||
#[pin]
|
||||
stream: S,
|
||||
}
|
||||
|
||||
@ -459,13 +436,12 @@ where
|
||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||
/// [`Stream`] ends.
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||
let mut stream: Pin<&mut S> = self.project().stream;
|
||||
loop {
|
||||
let stream = stream.as_mut();
|
||||
return Poll::Ready(match ready!(stream.poll_next(cx)) {
|
||||
let stream = &mut self.as_mut().stream;
|
||||
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
|
||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||
val => val,
|
||||
});
|
||||
@ -476,9 +452,9 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures_util::stream;
|
||||
use futures_util::future::poll_fn;
|
||||
use futures_util::pin_mut;
|
||||
use futures_util::stream;
|
||||
|
||||
impl Body {
|
||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
||||
@ -612,10 +588,6 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_body_eq() {
|
||||
assert!(Body::None == Body::None);
|
||||
assert!(Body::None != Body::Empty);
|
||||
assert!(Body::Empty == Body::Empty);
|
||||
assert!(Body::Empty != Body::None);
|
||||
assert!(
|
||||
Body::Bytes(Bytes::from_static(b"1"))
|
||||
== Body::Bytes(Bytes::from_static(b"1"))
|
||||
@ -627,7 +599,7 @@ mod tests {
|
||||
async fn test_body_debug() {
|
||||
assert!(format!("{:?}", Body::None).contains("Body::None"));
|
||||
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
|
||||
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains("1"));
|
||||
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@ -729,7 +701,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::<()>();
|
||||
|
@ -10,16 +10,16 @@ use crate::config::{KeepAlive, ServiceConfig};
|
||||
use crate::error::Error;
|
||||
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler};
|
||||
use crate::h2::H2Service;
|
||||
use crate::helpers::{Data, DataFactory};
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::service::HttpService;
|
||||
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>> {
|
||||
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
||||
keep_alive: KeepAlive,
|
||||
client_timeout: u64,
|
||||
client_disconnect: u64,
|
||||
@ -27,16 +27,16 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
_t: PhantomData<(T, S)>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler<T>>
|
||||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
/// Create instance of `ServiceConfigBuilder`
|
||||
pub fn new() -> Self {
|
||||
@ -48,26 +48,26 @@ where
|
||||
local_addr: None,
|
||||
expect: ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect: None,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: None,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
<X::Service as Service>::Future: 'static,
|
||||
U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
|
||||
<X::Service as Service<Request>>::Future: 'static,
|
||||
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
U::InitError: fmt::Debug,
|
||||
<U::Service as Service>::Future: 'static,
|
||||
<U::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
|
||||
{
|
||||
/// Set server keep-alive setting.
|
||||
///
|
||||
@ -123,11 +123,11 @@ where
|
||||
/// request will be forwarded to main service.
|
||||
pub fn expect<F, X1>(self, expect: F) -> HttpServiceBuilder<T, S, X1, U>
|
||||
where
|
||||
F: IntoServiceFactory<X1>,
|
||||
X1: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
F: IntoServiceFactory<X1, Request>,
|
||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X1::Error: Into<Error>,
|
||||
X1::InitError: fmt::Debug,
|
||||
<X1::Service as Service>::Future: 'static,
|
||||
<X1::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
@ -137,8 +137,8 @@ where
|
||||
local_addr: self.local_addr,
|
||||
expect: expect.into_factory(),
|
||||
upgrade: self.upgrade,
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,15 +148,11 @@ where
|
||||
/// and this service get called with original request and framed object.
|
||||
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
|
||||
where
|
||||
F: IntoServiceFactory<U1>,
|
||||
U1: ServiceFactory<
|
||||
Config = (),
|
||||
Request = (Request, Framed<T, Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
F: IntoServiceFactory<U1, (Request, Framed<T, Codec>)>,
|
||||
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
||||
U1::Error: fmt::Display,
|
||||
U1::InitError: fmt::Debug,
|
||||
<U1::Service as Service>::Future: 'static,
|
||||
<U1::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
@ -166,29 +162,29 @@ where
|
||||
local_addr: self.local_addr,
|
||||
expect: self.expect,
|
||||
upgrade: Some(upgrade.into_factory()),
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set on-connect callback.
|
||||
/// Sets the callback to be run on connection establishment.
|
||||
///
|
||||
/// It get called once per connection and result of the call
|
||||
/// get stored to the request's extensions.
|
||||
pub fn on_connect<F, I>(mut self, f: F) -> Self
|
||||
/// 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) -> I + 'static,
|
||||
I: Clone + 'static,
|
||||
F: Fn(&T, &mut Extensions) + 'static,
|
||||
{
|
||||
self.on_connect = Some(Rc::new(move |io| Box::new(Data(f(io)))));
|
||||
self.on_connect_ext = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Finish service configuration and create *http service* for HTTP/1 protocol.
|
||||
/// 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,
|
||||
F: IntoServiceFactory<S>,
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
@ -200,21 +196,22 @@ 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,
|
||||
F: IntoServiceFactory<S>,
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
@ -223,18 +220,20 @@ 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_ext(self.on_connect_ext)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create `HttpService` instance.
|
||||
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
F: IntoServiceFactory<S>,
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
@ -243,9 +242,10 @@ 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)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{fmt, io, mem, time};
|
||||
use std::{fmt, io, time};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use bytes::{Buf, Bytes};
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::{err, Either, FutureExt, LocalBoxFuture, Ready};
|
||||
use h2::client::SendRequest;
|
||||
use pin_project::pin_project;
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
@ -219,23 +223,13 @@ where
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
match self.project() {
|
||||
EitherIoProj::A(val) => val.poll_read(cx, buf),
|
||||
EitherIoProj::B(val) => val.poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn prepare_uninitialized_buffer(
|
||||
&self,
|
||||
buf: &mut [mem::MaybeUninit<u8>],
|
||||
) -> bool {
|
||||
match self {
|
||||
EitherIo::A(ref val) => val.prepare_uninitialized_buffer(buf),
|
||||
EitherIo::B(ref val) => val.prepare_uninitialized_buffer(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> AsyncWrite for EitherIo<A, B>
|
||||
@ -270,18 +264,4 @@ where
|
||||
EitherIoProj::B(val) => val.poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_write_buf<U: Buf>(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut U,
|
||||
) -> Poll<Result<usize, io::Error>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self.project() {
|
||||
EitherIoProj::A(val) => val.poll_write_buf(cx, buf),
|
||||
EitherIoProj::B(val) => val.poll_write_buf(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,11 @@ use std::marker::PhantomData;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_connect::{
|
||||
use actix_rt::net::TcpStream;
|
||||
use actix_service::{apply_fn, Service, ServiceExt};
|
||||
use actix_tls::connect::{
|
||||
default_connector, Connect as TcpConnect, Connection as TcpConnection,
|
||||
};
|
||||
use actix_rt::net::TcpStream;
|
||||
use actix_service::{apply_fn, Service};
|
||||
use actix_utils::timeout::{TimeoutError, TimeoutService};
|
||||
use http::Uri;
|
||||
|
||||
@ -18,10 +18,10 @@ use super::pool::{ConnectionPool, Protocol};
|
||||
use super::Connect;
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
use actix_connect::ssl::openssl::SslConnector as OpensslConnector;
|
||||
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
use actix_connect::ssl::rustls::ClientConfig;
|
||||
use actix_tls::connect::ssl::rustls::ClientConfig;
|
||||
#[cfg(feature = "rustls")]
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -52,7 +52,7 @@ pub struct Connector<T, U> {
|
||||
config: ConnectorConfig,
|
||||
#[allow(dead_code)]
|
||||
ssl: SslConnector,
|
||||
_t: PhantomData<U>,
|
||||
_phantom: PhantomData<U>,
|
||||
}
|
||||
|
||||
trait Io: AsyncRead + AsyncWrite + Unpin {}
|
||||
@ -62,9 +62,9 @@ impl Connector<(), ()> {
|
||||
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
|
||||
pub fn new() -> Connector<
|
||||
impl Service<
|
||||
Request = TcpConnect<Uri>,
|
||||
TcpConnect<Uri>,
|
||||
Response = TcpConnection<Uri, TcpStream>,
|
||||
Error = actix_connect::ConnectError,
|
||||
Error = actix_tls::connect::ConnectError,
|
||||
> + Clone,
|
||||
TcpStream,
|
||||
> {
|
||||
@ -72,14 +72,14 @@ impl Connector<(), ()> {
|
||||
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
|
||||
connector: default_connector(),
|
||||
config: ConnectorConfig::default(),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
// Build Ssl connector with openssl, based on supplied alpn protocols
|
||||
#[cfg(feature = "openssl")]
|
||||
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
|
||||
use actix_connect::ssl::openssl::SslMethod;
|
||||
use actix_tls::connect::ssl::openssl::SslMethod;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
let mut alpn = BytesMut::with_capacity(20);
|
||||
@ -102,7 +102,7 @@ impl Connector<(), ()> {
|
||||
config.set_protocols(&protocols);
|
||||
config
|
||||
.root_store
|
||||
.add_server_trust_anchors(&actix_tls::rustls::TLS_SERVER_ROOTS);
|
||||
.add_server_trust_anchors(&actix_tls::accept::rustls::TLS_SERVER_ROOTS);
|
||||
SslConnector::Rustls(Arc::new(config))
|
||||
}
|
||||
|
||||
@ -117,16 +117,16 @@ impl<T, U> Connector<T, U> {
|
||||
where
|
||||
U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug,
|
||||
T1: Service<
|
||||
Request = TcpConnect<Uri>,
|
||||
TcpConnect<Uri>,
|
||||
Response = TcpConnection<Uri, U1>,
|
||||
Error = actix_connect::ConnectError,
|
||||
Error = actix_tls::connect::ConnectError,
|
||||
> + Clone,
|
||||
{
|
||||
Connector {
|
||||
connector,
|
||||
config: self.config,
|
||||
ssl: self.ssl,
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -135,9 +135,9 @@ impl<T, U> Connector<T, U>
|
||||
where
|
||||
U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static,
|
||||
T: Service<
|
||||
Request = TcpConnect<Uri>,
|
||||
TcpConnect<Uri>,
|
||||
Response = TcpConnection<Uri, U>,
|
||||
Error = actix_connect::ConnectError,
|
||||
Error = actix_tls::connect::ConnectError,
|
||||
> + Clone
|
||||
+ 'static,
|
||||
{
|
||||
@ -241,8 +241,8 @@ where
|
||||
/// its combinator chain.
|
||||
pub fn finish(
|
||||
self,
|
||||
) -> impl Service<Request = Connect, Response = impl Connection, Error = ConnectError>
|
||||
+ Clone {
|
||||
) -> impl Service<Connect, Response = impl Connection, Error = ConnectError> + Clone
|
||||
{
|
||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
||||
{
|
||||
let connector = TimeoutService::new(
|
||||
@ -268,11 +268,11 @@ where
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
{
|
||||
const H2: &[u8] = b"h2";
|
||||
#[cfg(feature = "openssl")]
|
||||
use actix_connect::ssl::openssl::OpensslConnector;
|
||||
#[cfg(feature = "rustls")]
|
||||
use actix_connect::ssl::rustls::{RustlsConnector, Session};
|
||||
use actix_service::{boxed::service, pipeline};
|
||||
#[cfg(feature = "openssl")]
|
||||
use actix_tls::connect::ssl::openssl::OpensslConnector;
|
||||
#[cfg(feature = "rustls")]
|
||||
use actix_tls::connect::ssl::rustls::{RustlsConnector, Session};
|
||||
|
||||
let ssl_service = TimeoutService::new(
|
||||
self.config.timeout,
|
||||
@ -363,8 +363,7 @@ mod connect_impl {
|
||||
pub(crate) struct InnerConnector<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
pub(crate) tcp_pool: ConnectionPool<T, Io>,
|
||||
}
|
||||
@ -372,8 +371,7 @@ mod connect_impl {
|
||||
impl<T, Io> Clone for InnerConnector<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
InnerConnector {
|
||||
@ -382,17 +380,15 @@ mod connect_impl {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Io> Service for InnerConnector<T, Io>
|
||||
impl<T, Io> Service<Connect> for InnerConnector<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
type Request = Connect;
|
||||
type Response = IoConnection<Io>;
|
||||
type Error = ConnectError;
|
||||
type Future = Either<
|
||||
<ConnectionPool<T, Io> as Service>::Future,
|
||||
<ConnectionPool<T, Io> as Service<Connect>>::Future,
|
||||
Ready<Result<IoConnection<Io>, ConnectError>>,
|
||||
>;
|
||||
|
||||
@ -428,8 +424,8 @@ mod connect_impl {
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>,
|
||||
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>,
|
||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
|
||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
|
||||
{
|
||||
pub(crate) tcp_pool: ConnectionPool<T1, Io1>,
|
||||
pub(crate) ssl_pool: ConnectionPool<T2, Io2>,
|
||||
@ -439,10 +435,8 @@ mod connect_impl {
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
InnerConnector {
|
||||
@ -452,16 +446,13 @@ mod connect_impl {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T1, T2, Io1, Io2> Service for InnerConnector<T1, T2, Io1, Io2>
|
||||
impl<T1, T2, Io1, Io2> Service<Connect> for InnerConnector<T1, T2, Io1, Io2>
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
type Request = Connect;
|
||||
type Response = EitherConnection<Io1, Io2>;
|
||||
type Error = ConnectError;
|
||||
type Future = Either<
|
||||
@ -477,11 +468,11 @@ mod connect_impl {
|
||||
match req.uri.scheme_str() {
|
||||
Some("https") | Some("wss") => Either::Right(InnerConnectorResponseB {
|
||||
fut: self.ssl_pool.call(req),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
_ => Either::Left(InnerConnectorResponseA {
|
||||
fut: self.tcp_pool.call(req),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -491,18 +482,16 @@ mod connect_impl {
|
||||
pub(crate) struct InnerConnectorResponseA<T, Io1, Io2>
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
#[pin]
|
||||
fut: <ConnectionPool<T, Io1> as Service>::Future,
|
||||
_t: PhantomData<Io2>,
|
||||
fut: <ConnectionPool<T, Io1> as Service<Connect>>::Future,
|
||||
_phantom: PhantomData<Io2>,
|
||||
}
|
||||
|
||||
impl<T, Io1, Io2> Future for InnerConnectorResponseA<T, Io1, Io2>
|
||||
where
|
||||
T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
@ -520,18 +509,16 @@ mod connect_impl {
|
||||
pub(crate) struct InnerConnectorResponseB<T, Io1, Io2>
|
||||
where
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
#[pin]
|
||||
fut: <ConnectionPool<T, Io2> as Service>::Future,
|
||||
_t: PhantomData<Io1>,
|
||||
fut: <ConnectionPool<T, Io2> as Service<Connect>>::Future,
|
||||
_phantom: PhantomData<Io1>,
|
||||
}
|
||||
|
||||
impl<T, Io1, Io2> Future for InnerConnectorResponseB<T, Io1, Io2>
|
||||
where
|
||||
T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
|
@ -1,10 +1,10 @@
|
||||
use std::io;
|
||||
|
||||
use actix_connect::resolver::ResolveError;
|
||||
use actix_tls::connect::resolver::ResolveError;
|
||||
use derive_more::{Display, From};
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
use actix_connect::ssl::openssl::{HandshakeError, SslError};
|
||||
use actix_tls::accept::openssl::SslError;
|
||||
|
||||
use crate::error::{Error, ParseError, ResponseError};
|
||||
use crate::http::{Error as HttpError, StatusCode};
|
||||
@ -21,11 +21,6 @@ pub enum ConnectError {
|
||||
#[display(fmt = "{}", _0)]
|
||||
SslError(SslError),
|
||||
|
||||
/// SSL Handshake error
|
||||
#[cfg(feature = "openssl")]
|
||||
#[display(fmt = "{}", _0)]
|
||||
SslHandshakeError(String),
|
||||
|
||||
/// Failed to resolve the hostname
|
||||
#[display(fmt = "Failed resolving hostname: {}", _0)]
|
||||
Resolver(ResolveError),
|
||||
@ -57,25 +52,18 @@ pub enum ConnectError {
|
||||
|
||||
impl std::error::Error for ConnectError {}
|
||||
|
||||
impl From<actix_connect::ConnectError> for ConnectError {
|
||||
fn from(err: actix_connect::ConnectError) -> ConnectError {
|
||||
impl From<actix_tls::connect::ConnectError> for ConnectError {
|
||||
fn from(err: actix_tls::connect::ConnectError) -> ConnectError {
|
||||
match err {
|
||||
actix_connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
|
||||
actix_connect::ConnectError::NoRecords => ConnectError::NoRecords,
|
||||
actix_connect::ConnectError::InvalidInput => panic!(),
|
||||
actix_connect::ConnectError::Unresolved => ConnectError::Unresolved,
|
||||
actix_connect::ConnectError::Io(e) => ConnectError::Io(e),
|
||||
actix_tls::connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
|
||||
actix_tls::connect::ConnectError::NoRecords => ConnectError::NoRecords,
|
||||
actix_tls::connect::ConnectError::InvalidInput => panic!(),
|
||||
actix_tls::connect::ConnectError::Unresolved => ConnectError::Unresolved,
|
||||
actix_tls::connect::ConnectError::Io(e) => ConnectError::Io(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
impl<T: std::fmt::Debug> From<HandshakeError<T>> for ConnectError {
|
||||
fn from(err: HandshakeError<T>) -> ConnectError {
|
||||
ConnectError::SslHandshakeError(format!("{:?}", err))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, From)]
|
||||
pub enum InvalidUrl {
|
||||
#[display(fmt = "Missing url scheme")]
|
||||
|
@ -1,10 +1,10 @@
|
||||
use std::io::Write;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{io, mem, time};
|
||||
use std::{io, time};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use bytes::buf::BufMutExt;
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
|
||||
use bytes::buf::BufMut;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::Stream;
|
||||
use futures_util::future::poll_fn;
|
||||
@ -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?,
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {}
|
||||
_ => 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(
|
||||
@ -202,18 +204,11 @@ where
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncRead for H1Connection<T> {
|
||||
unsafe fn prepare_uninitialized_buffer(
|
||||
&self,
|
||||
buf: &mut [mem::MaybeUninit<u8>],
|
||||
) -> bool {
|
||||
self.io.as_ref().unwrap().prepare_uninitialized_buffer(buf)
|
||||
}
|
||||
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
@ -242,14 +237,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 +260,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 +279,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()
|
||||
}
|
||||
}
|
||||
|
@ -37,10 +37,10 @@ where
|
||||
trace!("Sending client request: {:?} {:?}", head, body.size());
|
||||
let head_req = head.as_ref().method == Method::HEAD;
|
||||
let length = body.size();
|
||||
let eof = match length {
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0) => true,
|
||||
_ => false,
|
||||
};
|
||||
let eof = matches!(
|
||||
length,
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0)
|
||||
);
|
||||
|
||||
let mut req = Request::new(());
|
||||
*req.uri_mut() = head.as_ref().uri.clone();
|
||||
@ -89,7 +89,7 @@ where
|
||||
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
|
||||
CONTENT_LENGTH if skip_len => continue,
|
||||
// DATE => has_date = true,
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
req.headers_mut().append(key, value.clone());
|
||||
}
|
||||
|
@ -2,15 +2,16 @@ 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};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::time::{delay_for, Delay};
|
||||
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use actix_rt::time::{sleep, Sleep};
|
||||
use actix_service::Service;
|
||||
use actix_utils::{oneshot, task::LocalWaker};
|
||||
use actix_utils::task::LocalWaker;
|
||||
use bytes::Bytes;
|
||||
use futures_channel::oneshot;
|
||||
use futures_util::future::{poll_fn, FutureExt, LocalBoxFuture};
|
||||
use fxhash::FxHashMap;
|
||||
use h2::client::{Connection, SendRequest};
|
||||
@ -49,8 +50,7 @@ pub(crate) struct ConnectionPool<T, Io: 'static>(Rc<RefCell<T>>, Rc<RefCell<Inne
|
||||
impl<T, Io> ConnectionPool<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
pub(crate) fn new(connector: T, config: ConnectorConfig) -> Self {
|
||||
let connector_rc = Rc::new(RefCell::new(connector));
|
||||
@ -65,14 +65,11 @@ 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,
|
||||
)
|
||||
ConnectionPool(connector_rc, inner_rc)
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,13 +82,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Io> Service for ConnectionPool<T, Io>
|
||||
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<Connect> for ConnectionPool<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
type Request = Connect;
|
||||
type Response = IoConnection<Io>;
|
||||
type Error = ConnectError;
|
||||
type Future = LocalBoxFuture<'static, Result<IoConnection<Io>, ConnectError>>;
|
||||
@ -115,11 +117,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
|
||||
@ -329,10 +331,11 @@ where
|
||||
} else {
|
||||
let mut io = conn.io;
|
||||
let mut buf = [0; 2];
|
||||
let mut read_buf = ReadBuf::new(&mut buf);
|
||||
if let ConnectionType::H1(ref mut s) = io {
|
||||
match Pin::new(s).poll_read(cx, &mut buf) {
|
||||
Poll::Pending => (),
|
||||
Poll::Ready(Ok(n)) if n > 0 => {
|
||||
match Pin::new(s).poll_read(cx, &mut read_buf) {
|
||||
Poll::Pending => {}
|
||||
Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => {
|
||||
if let Some(timeout) = self.config.disconnect_timeout {
|
||||
if let ConnectionType::H1(io) = io {
|
||||
actix_rt::spawn(CloseConnection::new(
|
||||
@ -382,9 +385,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
struct CloseConnection<T> {
|
||||
io: T,
|
||||
timeout: Delay,
|
||||
#[pin]
|
||||
timeout: Sleep,
|
||||
}
|
||||
|
||||
impl<T> CloseConnection<T>
|
||||
@ -394,7 +399,7 @@ where
|
||||
fn new(io: T, timeout: Duration) -> Self {
|
||||
CloseConnection {
|
||||
io,
|
||||
timeout: delay_for(timeout),
|
||||
timeout: sleep(timeout),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -406,11 +411,11 @@ where
|
||||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
||||
let this = self.get_mut();
|
||||
let this = self.project();
|
||||
|
||||
match Pin::new(&mut this.timeout).poll(cx) {
|
||||
match this.timeout.poll(cx) {
|
||||
Poll::Ready(_) => Poll::Ready(()),
|
||||
Poll::Pending => match Pin::new(&mut this.io).poll_shutdown(cx) {
|
||||
Poll::Pending => match Pin::new(this.io).poll_shutdown(cx) {
|
||||
Poll::Ready(_) => Poll::Ready(()),
|
||||
Poll::Pending => Poll::Pending,
|
||||
},
|
||||
@ -424,13 +429,13 @@ 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>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError>,
|
||||
T::Future: 'static,
|
||||
{
|
||||
type Output = ();
|
||||
@ -438,55 +443,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
|
||||
}
|
||||
}
|
||||
|
||||
|
0
actix-http/src/clinu/mod.rs
Normal file
0
actix-http/src/clinu/mod.rs
Normal file
@ -1,40 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_service::Service;
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Service that allows to turn non-clone service to a service with `Clone` impl
|
||||
///
|
||||
/// # Panics
|
||||
/// CloneableService might panic with some creative use of thread local storage.
|
||||
/// See https://github.com/actix/actix-web/issues/1295 for example
|
||||
pub(crate) struct CloneableService<T: Service>(Rc<RefCell<T>>);
|
||||
|
||||
impl<T: Service> CloneableService<T> {
|
||||
pub(crate) fn new(service: T) -> Self {
|
||||
Self(Rc::new(RefCell::new(service)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Service> Clone for CloneableService<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Service> Service for CloneableService<T> {
|
||||
type Request = T::Request;
|
||||
type Response = T::Response;
|
||||
type Error = T::Error;
|
||||
type Future = T::Future;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.0.borrow_mut().poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: T::Request) -> Self::Future {
|
||||
self.0.borrow_mut().call(req)
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use std::{fmt, net};
|
||||
|
||||
use actix_rt::time::{delay_for, delay_until, Delay, Instant};
|
||||
use actix_rt::time::{sleep, sleep_until, Instant, Sleep};
|
||||
use bytes::BytesMut;
|
||||
use futures_util::{future, FutureExt};
|
||||
use time::OffsetDateTime;
|
||||
@ -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,
|
||||
@ -121,10 +121,10 @@ impl ServiceConfig {
|
||||
|
||||
#[inline]
|
||||
/// Client timeout for first request.
|
||||
pub fn client_timer(&self) -> Option<Delay> {
|
||||
pub fn client_timer(&self) -> Option<Sleep> {
|
||||
let delay_time = self.0.client_timeout;
|
||||
if delay_time != 0 {
|
||||
Some(delay_until(
|
||||
Some(sleep_until(
|
||||
self.0.timer.now() + Duration::from_millis(delay_time),
|
||||
))
|
||||
} else {
|
||||
@ -154,9 +154,9 @@ impl ServiceConfig {
|
||||
|
||||
#[inline]
|
||||
/// Return keep-alive timer delay is configured.
|
||||
pub fn keep_alive_timer(&self) -> Option<Delay> {
|
||||
pub fn keep_alive_timer(&self) -> Option<Sleep> {
|
||||
if let Some(ka) = self.0.keep_alive {
|
||||
Some(delay_until(self.0.timer.now() + ka))
|
||||
Some(sleep_until(self.0.timer.now() + ka))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -209,6 +209,7 @@ impl Date {
|
||||
date.update();
|
||||
date
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.pos = 0;
|
||||
write!(
|
||||
@ -265,7 +266,7 @@ impl DateService {
|
||||
|
||||
// periodic date update
|
||||
let s = self.clone();
|
||||
actix_rt::spawn(delay_for(Duration::from_millis(500)).then(move |_| {
|
||||
actix_rt::spawn(sleep(Duration::from_millis(500)).then(move |_| {
|
||||
s.0.reset();
|
||||
future::ready(())
|
||||
}));
|
||||
|
@ -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};
|
||||
@ -24,7 +25,7 @@ pub use crate::cookie::ParseError as CookieParseError;
|
||||
use crate::helpers::Writer;
|
||||
use crate::response::{Response, ResponseBuilder};
|
||||
|
||||
/// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html)
|
||||
/// A specialized [`std::result::Result`]
|
||||
/// for actix web operations
|
||||
///
|
||||
/// This typedef is generally used to avoid writing out
|
||||
@ -177,11 +178,7 @@ impl ResponseError for FormError {}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
/// `InternalServerError` for `openssl::ssl::Error`
|
||||
impl ResponseError for actix_connect::ssl::openssl::SslError {}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
/// `InternalServerError` for `openssl::ssl::HandshakeError`
|
||||
impl<T: std::fmt::Debug> ResponseError for actix_tls::openssl::HandshakeError<T> {}
|
||||
impl ResponseError for actix_tls::accept::openssl::SslError {}
|
||||
|
||||
/// Return `BAD_REQUEST` for `de::value::Error`
|
||||
impl ResponseError for DeError {
|
||||
@ -452,10 +449,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,
|
||||
{
|
||||
}
|
||||
@ -955,16 +952,10 @@ where
|
||||
/// This is supported on feature=`actors` only
|
||||
impl ResponseError for actix::MailboxError {}
|
||||
|
||||
#[cfg(feature = "actors")]
|
||||
/// `InternalServerError` for `actix::ResolverError`
|
||||
/// This is supported on feature=`actors` only
|
||||
impl ResponseError for actix::actors::resolver::ResolverError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::{Error as HttpError, StatusCode};
|
||||
use httparse;
|
||||
use std::io;
|
||||
|
||||
#[test]
|
||||
|
@ -1,10 +1,10 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::fmt;
|
||||
use std::{fmt, mem};
|
||||
|
||||
use fxhash::FxHashMap;
|
||||
|
||||
#[derive(Default)]
|
||||
/// A type map of request extensions.
|
||||
#[derive(Default)]
|
||||
pub struct Extensions {
|
||||
/// Use FxHasher with a std HashMap with for faster
|
||||
/// lookups on the small `TypeId` (u64 equivalent) keys.
|
||||
@ -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 {
|
||||
@ -72,7 +82,7 @@ impl fmt::Debug for Extensions {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_remove() {
|
||||
let mut map = Extensions::new();
|
||||
@ -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 {
|
||||
|
@ -58,6 +58,7 @@ impl Codec {
|
||||
} else {
|
||||
Flags::empty()
|
||||
};
|
||||
|
||||
Codec {
|
||||
config,
|
||||
flags,
|
||||
@ -69,26 +70,26 @@ impl Codec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if request is upgrade.
|
||||
#[inline]
|
||||
/// Check if request is upgrade
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.ctype == ConnectionType::Upgrade
|
||||
}
|
||||
|
||||
/// Check if last response is keep-alive.
|
||||
#[inline]
|
||||
/// Check if last response is keep-alive
|
||||
pub fn keepalive(&self) -> bool {
|
||||
self.ctype == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Check if keep-alive enabled on server level.
|
||||
#[inline]
|
||||
/// Check if keep-alive enabled on server level
|
||||
pub fn keepalive_enabled(&self) -> bool {
|
||||
self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
||||
}
|
||||
|
||||
/// Check last request's message type.
|
||||
#[inline]
|
||||
/// Check last request's message type
|
||||
pub fn message_type(&self) -> MessageType {
|
||||
if self.flags.contains(Flags::STREAM) {
|
||||
MessageType::Stream
|
||||
@ -110,8 +111,8 @@ impl Decoder for Codec {
|
||||
type Error = ParseError;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
if self.payload.is_some() {
|
||||
Ok(match self.payload.as_mut().unwrap().decode(src)? {
|
||||
if let Some(ref mut payload) = self.payload {
|
||||
Ok(match payload.decode(src)? {
|
||||
Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))),
|
||||
Some(PayloadItem::Eof) => {
|
||||
self.payload.take();
|
||||
@ -144,13 +145,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 {
|
||||
|
@ -1,7 +1,6 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::io;
|
||||
use std::marker::PhantomData;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::task::Poll;
|
||||
|
||||
use actix_codec::Decoder;
|
||||
@ -46,7 +45,7 @@ impl<T: MessageType> Decoder for MessageDecoder<T> {
|
||||
|
||||
pub(crate) enum PayloadLength {
|
||||
Payload(PayloadType),
|
||||
Upgrade,
|
||||
UpgradeWebSocket,
|
||||
None,
|
||||
}
|
||||
|
||||
@ -65,7 +64,7 @@ pub(crate) trait MessageType: Sized {
|
||||
raw_headers: &[HeaderIndex],
|
||||
) -> Result<PayloadLength, ParseError> {
|
||||
let mut ka = None;
|
||||
let mut has_upgrade = false;
|
||||
let mut has_upgrade_websocket = false;
|
||||
let mut expect = false;
|
||||
let mut chunked = false;
|
||||
let mut content_length = None;
|
||||
@ -77,12 +76,14 @@ pub(crate) trait MessageType: Sized {
|
||||
let name =
|
||||
HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap();
|
||||
|
||||
// Unsafe: httparse check header value for 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() {
|
||||
@ -124,12 +125,9 @@ pub(crate) trait MessageType: Sized {
|
||||
};
|
||||
}
|
||||
header::UPGRADE => {
|
||||
has_upgrade = true;
|
||||
// check content-length, some clients (dart)
|
||||
// sends "content-length: 0" with websocket upgrade
|
||||
if let Ok(val) = value.to_str().map(|val| val.trim()) {
|
||||
if val.eq_ignore_ascii_case("websocket") {
|
||||
content_length = None;
|
||||
has_upgrade_websocket = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,7 +137,7 @@ pub(crate) trait MessageType: Sized {
|
||||
expect = true;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
headers.append(name, value);
|
||||
@ -156,13 +154,13 @@ pub(crate) trait MessageType: Sized {
|
||||
Ok(PayloadLength::Payload(PayloadType::Payload(
|
||||
PayloadDecoder::chunked(),
|
||||
)))
|
||||
} else if has_upgrade_websocket {
|
||||
Ok(PayloadLength::UpgradeWebSocket)
|
||||
} else if let Some(len) = content_length {
|
||||
// Content-Length
|
||||
Ok(PayloadLength::Payload(PayloadType::Payload(
|
||||
PayloadDecoder::length(len),
|
||||
)))
|
||||
} else if has_upgrade {
|
||||
Ok(PayloadLength::Upgrade)
|
||||
} else {
|
||||
Ok(PayloadLength::None)
|
||||
}
|
||||
@ -184,16 +182,11 @@ impl MessageType for Request {
|
||||
&mut self.head_mut().headers
|
||||
}
|
||||
|
||||
#[allow(clippy::uninit_assumed_init)]
|
||||
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
|
||||
// Unsafe: we read only this data only after httparse parses headers into.
|
||||
// performance bump for pipeline benchmarks.
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||
|
||||
let (len, method, uri, ver, h_len) = {
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||
|
||||
let mut req = httparse::Request::new(&mut parsed);
|
||||
match req.parse(src)? {
|
||||
@ -222,7 +215,7 @@ impl MessageType for Request {
|
||||
// payload decoder
|
||||
let decoder = match length {
|
||||
PayloadLength::Payload(pl) => pl,
|
||||
PayloadLength::Upgrade => {
|
||||
PayloadLength::UpgradeWebSocket => {
|
||||
// upgrade(websocket)
|
||||
PayloadType::Stream(PayloadDecoder::eof())
|
||||
}
|
||||
@ -260,16 +253,11 @@ impl MessageType for ResponseHead {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
#[allow(clippy::uninit_assumed_init)]
|
||||
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
|
||||
// Unsafe: we read only this data only after httparse parses headers into.
|
||||
// performance bump for pipeline benchmarks.
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||
|
||||
let (len, ver, status, h_len) = {
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||
|
||||
let mut res = httparse::Response::new(&mut parsed);
|
||||
match res.parse(src)? {
|
||||
@ -324,6 +312,17 @@ pub(crate) struct HeaderIndex {
|
||||
pub(crate) value: (usize, usize),
|
||||
}
|
||||
|
||||
pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
||||
name: (0, 0),
|
||||
value: (0, 0),
|
||||
};
|
||||
|
||||
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||
|
||||
pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
||||
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||
|
||||
impl HeaderIndex {
|
||||
pub(crate) fn record(
|
||||
bytes: &[u8],
|
||||
@ -655,10 +654,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn is_unhandled(&self) -> bool {
|
||||
match self {
|
||||
PayloadType::Stream(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, PayloadType::Stream(_))
|
||||
}
|
||||
}
|
||||
|
||||
@ -670,10 +666,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
fn eof(&self) -> bool {
|
||||
match *self {
|
||||
PayloadItem::Eof => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(*self, PayloadItem::Eof)
|
||||
}
|
||||
}
|
||||
|
||||
@ -692,7 +685,7 @@ mod tests {
|
||||
match MessageDecoder::<Request>::default().decode($e) {
|
||||
Err(err) => match err {
|
||||
ParseError::Io(_) => unreachable!("Parse error expected"),
|
||||
_ => (),
|
||||
_ => {}
|
||||
},
|
||||
_ => unreachable!("Error expected"),
|
||||
}
|
||||
@ -979,7 +972,7 @@ mod tests {
|
||||
unreachable!("Error");
|
||||
}
|
||||
|
||||
// type in chunked
|
||||
// intentional typo in "chunked"
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
transfer-encoding: chnked\r\n\r\n",
|
||||
@ -1040,7 +1033,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_upgrade() {
|
||||
fn test_http_request_upgrade_websocket() {
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
connection: upgrade\r\n\
|
||||
@ -1054,6 +1047,26 @@ mod tests {
|
||||
assert!(pl.is_unhandled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_upgrade_h2c() {
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
connection: upgrade, http2-settings\r\n\
|
||||
upgrade: h2c\r\n\
|
||||
http2-settings: dummy\r\n\r\n",
|
||||
);
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||
// `connection: upgrade, http2-settings` doesn't work properly..
|
||||
// see MessageType::set_headers().
|
||||
//
|
||||
// The line below should be:
|
||||
// assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||
assert!(req.upgrade());
|
||||
assert!(!pl.is_unhandled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_parser_utf8() {
|
||||
let mut buf = BytesMut::from(
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -21,7 +21,7 @@ const AVERAGE_HEADER_SIZE: usize = 30;
|
||||
pub(crate) struct MessageEncoder<T: MessageType> {
|
||||
pub length: BodySize,
|
||||
pub te: TransferEncoding,
|
||||
_t: PhantomData<T>,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: MessageType> Default for MessageEncoder<T> {
|
||||
@ -29,7 +29,7 @@ impl<T: MessageType> Default for MessageEncoder<T> {
|
||||
MessageEncoder {
|
||||
length: BodySize::None,
|
||||
te: TransferEncoding::empty(),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -64,14 +64,17 @@ pub(crate) trait MessageType: Sized {
|
||||
// Content length
|
||||
if let Some(status) = self.status() {
|
||||
match status {
|
||||
StatusCode::NO_CONTENT
|
||||
| StatusCode::CONTINUE
|
||||
| StatusCode::PROCESSING => length = BodySize::None,
|
||||
StatusCode::SWITCHING_PROTOCOLS => {
|
||||
StatusCode::CONTINUE
|
||||
| StatusCode::SWITCHING_PROTOCOLS
|
||||
| StatusCode::PROCESSING
|
||||
| StatusCode::NO_CONTENT => {
|
||||
// skip content-length and transfer-encoding headers
|
||||
// See https://tools.ietf.org/html/rfc7230#section-3.3.1
|
||||
// and https://tools.ietf.org/html/rfc7230#section-3.3.2
|
||||
skip_len = true;
|
||||
length = BodySize::Stream;
|
||||
length = BodySize::None
|
||||
}
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
match length {
|
||||
@ -115,7 +118,7 @@ pub(crate) trait MessageType: Sized {
|
||||
dst.put_slice(b"connection: close\r\n")
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// merging headers from head and extra headers. HeaderMap::new() does not allocate.
|
||||
@ -129,89 +132,133 @@ pub(crate) trait MessageType: Sized {
|
||||
.chain(extra_headers.inner.iter());
|
||||
|
||||
// write headers
|
||||
let mut pos = 0;
|
||||
|
||||
let mut has_date = false;
|
||||
|
||||
let mut buf = dst.chunk_mut().as_mut_ptr() as *mut u8;
|
||||
let mut remaining = dst.capacity() - dst.len();
|
||||
let mut buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
|
||||
|
||||
// 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();
|
||||
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
|
||||
|
||||
// re-assign buf raw pointer since it's possible that the buffer was
|
||||
// reallocated and/or resized
|
||||
buf = dst.chunk_mut().as_mut_ptr() as *mut u8;
|
||||
}
|
||||
// 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();
|
||||
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
|
||||
|
||||
// re-assign buf raw pointer since it's possible that the buffer was
|
||||
// reallocated and/or resized
|
||||
buf = dst.chunk_mut().as_mut_ptr() as *mut u8;
|
||||
}
|
||||
// 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,35 +524,37 @@ 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);
|
||||
}
|
||||
|
||||
fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
||||
let mut index = 0;
|
||||
let key = value;
|
||||
let mut key_iter = key.iter();
|
||||
// first copy entire (potentially wrong) slice to output
|
||||
buffer[..value.len()].copy_from_slice(value);
|
||||
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
let mut iter = value.iter();
|
||||
|
||||
// first character should be uppercase
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[0] = c & 0b1101_1111;
|
||||
}
|
||||
|
||||
while let Some(c) = key_iter.next() {
|
||||
buffer[index] = *c;
|
||||
index += 1;
|
||||
if *c == b'-' {
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
// track 1 ahead of the current position since that's the location being assigned to
|
||||
let mut index = 2;
|
||||
|
||||
// remaining characters after hyphens should also be uppercase
|
||||
while let Some(&c) = iter.next() {
|
||||
if c == b'-' {
|
||||
// advance iter by one and uppercase if needed
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[index] = c & 0b1101_1111;
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -554,6 +603,8 @@ mod tests {
|
||||
);
|
||||
let data =
|
||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||
eprintln!("{}", &data);
|
||||
|
||||
assert!(data.contains("Content-Length: 0\r\n"));
|
||||
assert!(data.contains("Connection: close\r\n"));
|
||||
assert!(data.contains("Content-Type: plain/text\r\n"));
|
||||
@ -629,4 +680,28 @@ mod tests {
|
||||
assert!(data.contains("authorization: another authorization\r\n"));
|
||||
assert!(data.contains("date: date\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_content_length() {
|
||||
let mut bytes = BytesMut::with_capacity(2048);
|
||||
|
||||
let mut res: Response<()> =
|
||||
Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>();
|
||||
res.headers_mut()
|
||||
.insert(DATE, HeaderValue::from_static(&""));
|
||||
res.headers_mut()
|
||||
.insert(CONTENT_LENGTH, HeaderValue::from_static(&"0"));
|
||||
|
||||
let _ = res.encode_headers(
|
||||
&mut bytes,
|
||||
Version::HTTP_11,
|
||||
BodySize::Stream,
|
||||
ConnectionType::Upgrade,
|
||||
&ServiceConfig::default(),
|
||||
);
|
||||
let data =
|
||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||
assert!(!data.contains("content-length: 0\r\n"));
|
||||
assert!(!data.contains("transfer-encoding: chunked\r\n"));
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,27 @@
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use futures_util::future::{ok, Ready};
|
||||
use futures_util::future::{ready, Ready};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::request::Request;
|
||||
|
||||
pub struct ExpectHandler;
|
||||
|
||||
impl ServiceFactory for ExpectHandler {
|
||||
type Config = ();
|
||||
type Request = Request;
|
||||
impl ServiceFactory<Request> for ExpectHandler {
|
||||
type Response = Request;
|
||||
type Error = Error;
|
||||
type Config = ();
|
||||
type Service = ExpectHandler;
|
||||
type InitError = Error;
|
||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
ok(ExpectHandler)
|
||||
fn new_service(&self, _: Self::Config) -> Self::Future {
|
||||
ready(Ok(ExpectHandler))
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for ExpectHandler {
|
||||
type Request = Request;
|
||||
impl Service<Request> for ExpectHandler {
|
||||
type Response = Request;
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
@ -33,6 +31,8 @@ impl Service for ExpectHandler {
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request) -> Self::Future {
|
||||
ok(req)
|
||||
ready(Ok(req))
|
||||
// TODO: add some way to trigger error
|
||||
// Err(error::ErrorExpectationFailed("test"))
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ pub use self::codec::Codec;
|
||||
pub use self::dispatcher::Dispatcher;
|
||||
pub use self::expect::ExpectHandler;
|
||||
pub use self::payload::Payload;
|
||||
pub use self::service::{H1Service, H1ServiceHandler, OneRequest};
|
||||
pub use self::service::{H1Service, H1ServiceHandler};
|
||||
pub use self::upgrade::UpgradeHandler;
|
||||
pub use self::utils::SendResponse;
|
||||
|
||||
|
@ -182,9 +182,7 @@ impl Inner {
|
||||
self.len += data.len();
|
||||
self.items.push_back(data);
|
||||
self.need_read = self.len < MAX_BUFFER_SIZE;
|
||||
if let Some(task) = self.task.take() {
|
||||
task.wake()
|
||||
}
|
||||
self.task.wake();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,3 +1,4 @@
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
@ -9,40 +10,40 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_rt::net::TcpStream;
|
||||
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory};
|
||||
use futures_core::ready;
|
||||
use futures_util::future::{ok, Ready};
|
||||
use futures_util::future::ready;
|
||||
|
||||
use crate::body::MessageBody;
|
||||
use crate::cloneable::CloneableService;
|
||||
use crate::config::ServiceConfig;
|
||||
use crate::error::{DispatchError, Error, ParseError};
|
||||
use crate::helpers::DataFactory;
|
||||
use crate::error::{DispatchError, Error};
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::service::HttpFlow;
|
||||
use crate::{ConnectCallback, OnConnectData};
|
||||
|
||||
use super::codec::Codec;
|
||||
use super::dispatcher::Dispatcher;
|
||||
use super::{ExpectHandler, Message, UpgradeHandler};
|
||||
use super::{ExpectHandler, UpgradeHandler};
|
||||
|
||||
/// `ServiceFactory` implementation for HTTP1 transport
|
||||
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler<T>> {
|
||||
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
|
||||
srv: S,
|
||||
cfg: ServiceConfig,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<T, S, B> H1Service<T, S, B>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
B: MessageBody,
|
||||
{
|
||||
/// Create new `HttpService` instance with config.
|
||||
pub(crate) fn with_config<F: IntoServiceFactory<S>>(
|
||||
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
|
||||
cfg: ServiceConfig,
|
||||
service: F,
|
||||
) -> Self {
|
||||
@ -51,27 +52,23 @@ where
|
||||
srv: service.into_factory(),
|
||||
expect: ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect: None,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: None,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
B: MessageBody,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
U: ServiceFactory<
|
||||
Config = (),
|
||||
Request = (Request, Framed<TcpStream, Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
@ -79,15 +76,15 @@ where
|
||||
pub fn tcp(
|
||||
self,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = DispatchError,
|
||||
InitError = (),
|
||||
> {
|
||||
pipeline_factory(|io: TcpStream| {
|
||||
let peer_addr = io.peer_addr().ok();
|
||||
ok((io, peer_addr))
|
||||
ready(Ok((io, peer_addr)))
|
||||
})
|
||||
.and_then(self)
|
||||
}
|
||||
@ -97,22 +94,23 @@ where
|
||||
mod openssl {
|
||||
use super::*;
|
||||
|
||||
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
|
||||
use actix_tls::{openssl::HandshakeError, SslError};
|
||||
use actix_service::ServiceFactoryExt;
|
||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
|
||||
impl<S, B, X, U> H1Service<SslStream<TcpStream>, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
B: MessageBody,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
U: ServiceFactory<
|
||||
(Request, Framed<SslStream<TcpStream>, Codec>),
|
||||
Config = (),
|
||||
Request = (Request, Framed<SslStream<TcpStream>, Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
@ -123,22 +121,22 @@ mod openssl {
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<HandshakeError<TcpStream>, DispatchError>,
|
||||
Error = TlsError<SslError, 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))
|
||||
ready(Ok((io, peer_addr)))
|
||||
})
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -146,23 +144,24 @@ mod openssl {
|
||||
#[cfg(feature = "rustls")]
|
||||
mod rustls {
|
||||
use super::*;
|
||||
use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
|
||||
use actix_tls::SslError;
|
||||
use actix_service::ServiceFactoryExt;
|
||||
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
use std::{fmt, io};
|
||||
|
||||
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
B: MessageBody,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
U: ServiceFactory<
|
||||
(Request, Framed<TlsStream<TcpStream>, Codec>),
|
||||
Config = (),
|
||||
Request = (Request, Framed<TlsStream<TcpStream>, Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
@ -173,29 +172,29 @@ mod rustls {
|
||||
self,
|
||||
config: ServerConfig,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
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))
|
||||
ready(Ok((io, peer_addr)))
|
||||
})
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error>,
|
||||
S::Response: Into<Response<B>>,
|
||||
S::InitError: fmt::Debug,
|
||||
@ -203,7 +202,7 @@ where
|
||||
{
|
||||
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
|
||||
where
|
||||
X1: ServiceFactory<Request = Request, Response = Request>,
|
||||
X1: ServiceFactory<Request, Response = Request>,
|
||||
X1::Error: Into<Error>,
|
||||
X1::InitError: fmt::Debug,
|
||||
{
|
||||
@ -212,14 +211,14 @@ where
|
||||
cfg: self.cfg,
|
||||
srv: self.srv,
|
||||
upgrade: self.upgrade,
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn upgrade<U1>(self, upgrade: Option<U1>) -> H1Service<T, S, B, X, U1>
|
||||
where
|
||||
U1: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
|
||||
U1: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U1::Error: fmt::Display,
|
||||
U1::InitError: fmt::Debug,
|
||||
{
|
||||
@ -228,42 +227,39 @@ where
|
||||
cfg: self.cfg,
|
||||
srv: self.srv,
|
||||
expect: self.expect,
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set on connect callback.
|
||||
pub(crate) fn on_connect(
|
||||
mut self,
|
||||
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
) -> Self {
|
||||
self.on_connect = f;
|
||||
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>
|
||||
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
|
||||
for H1Service<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error>,
|
||||
S::Response: Into<Response<B>>,
|
||||
S::InitError: fmt::Debug,
|
||||
B: MessageBody,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
|
||||
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
type Config = ();
|
||||
type Request = (T, Option<net::SocketAddr>);
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
type InitError = ();
|
||||
type Config = ();
|
||||
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
|
||||
type InitError = ();
|
||||
type Future = H1ServiceResponse<T, S, B, X, U>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
@ -273,9 +269,9 @@ where
|
||||
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())),
|
||||
expect: None,
|
||||
upgrade: None,
|
||||
on_connect: self.on_connect.clone(),
|
||||
on_connect_ext: self.on_connect_ext.clone(),
|
||||
cfg: Some(self.cfg.clone()),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -284,13 +280,13 @@ where
|
||||
#[pin_project::pin_project]
|
||||
pub struct H1ServiceResponse<T, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Request = Request>,
|
||||
S: ServiceFactory<Request>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
X: ServiceFactory<Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
U: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
|
||||
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
@ -302,23 +298,23 @@ where
|
||||
fut_upg: Option<U::Future>,
|
||||
expect: Option<X::Service>,
|
||||
upgrade: Option<U::Service>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
cfg: Option<ServiceConfig>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> Future for H1ServiceResponse<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: ServiceFactory<Request = Request>,
|
||||
S: ServiceFactory<Request>,
|
||||
S::Error: Into<Error>,
|
||||
S::Response: Into<Response<B>>,
|
||||
S::InitError: fmt::Debug,
|
||||
B: MessageBody,
|
||||
X: ServiceFactory<Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
U: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
|
||||
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
@ -342,7 +338,7 @@ where
|
||||
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
|
||||
this = self.as_mut().project();
|
||||
*this.upgrade = Some(upgrade);
|
||||
this.fut_ex.set(None);
|
||||
this.fut_upg.set(None);
|
||||
}
|
||||
|
||||
let result = ready!(this
|
||||
@ -352,75 +348,78 @@ 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
|
||||
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>>>,
|
||||
/// `Service` implementation for HTTP/1 transport
|
||||
pub struct H1ServiceHandler<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
X: Service<Request>,
|
||||
U: Service<(Request, Framed<T, Codec>)>,
|
||||
{
|
||||
flow: Rc<RefCell<HttpFlow<S, X, U>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
cfg: ServiceConfig,
|
||||
_t: PhantomData<(T, B)>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> H1ServiceHandler<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error>,
|
||||
S::Response: Into<Response<B>>,
|
||||
B: MessageBody,
|
||||
X: Service<Request = Request, Response = Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
fn new(
|
||||
cfg: ServiceConfig,
|
||||
srv: S,
|
||||
service: S,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
) -> H1ServiceHandler<T, S, B, X, U> {
|
||||
H1ServiceHandler {
|
||||
srv: CloneableService::new(srv),
|
||||
expect: CloneableService::new(expect),
|
||||
upgrade: upgrade.map(CloneableService::new),
|
||||
flow: HttpFlow::new(service, expect, upgrade),
|
||||
cfg,
|
||||
on_connect,
|
||||
_t: PhantomData,
|
||||
on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> Service for H1ServiceHandler<T, S, B, X, U>
|
||||
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
|
||||
for H1ServiceHandler<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error>,
|
||||
S::Response: Into<Response<B>>,
|
||||
B: MessageBody,
|
||||
X: Service<Request = Request, Response = Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
{
|
||||
type Request = (T, Option<net::SocketAddr>);
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
type Future = Dispatcher<T, S, B, X, U>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
let ready = self
|
||||
let mut flow = self.flow.borrow_mut();
|
||||
let ready = flow
|
||||
.expect
|
||||
.poll_ready(cx)
|
||||
.map_err(|e| {
|
||||
@ -430,8 +429,8 @@ where
|
||||
})?
|
||||
.is_ready();
|
||||
|
||||
let ready = self
|
||||
.srv
|
||||
let ready = flow
|
||||
.service
|
||||
.poll_ready(cx)
|
||||
.map_err(|e| {
|
||||
let e = e.into();
|
||||
@ -441,7 +440,7 @@ where
|
||||
.is_ready()
|
||||
&& ready;
|
||||
|
||||
let ready = if let Some(ref mut upg) = self.upgrade {
|
||||
let ready = if let Some(ref mut upg) = flow.upgrade {
|
||||
upg.poll_ready(cx)
|
||||
.map_err(|e| {
|
||||
let e = e.into();
|
||||
@ -461,117 +460,16 @@ 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
|
||||
};
|
||||
fn call(&mut self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||
let on_connect_data =
|
||||
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||
|
||||
Dispatcher::new(
|
||||
io,
|
||||
self.cfg.clone(),
|
||||
self.srv.clone(),
|
||||
self.expect.clone(),
|
||||
self.upgrade.clone(),
|
||||
on_connect,
|
||||
self.flow.clone(),
|
||||
on_connect_data,
|
||||
addr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// `ServiceFactory` implementation for `OneRequestService` service
|
||||
#[derive(Default)]
|
||||
pub struct OneRequest<T> {
|
||||
config: ServiceConfig,
|
||||
_t: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> OneRequest<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
/// Create new `H1SimpleService` instance.
|
||||
pub fn new() -> Self {
|
||||
OneRequest {
|
||||
config: ServiceConfig::default(),
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ServiceFactory for OneRequest<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Config = ();
|
||||
type Request = T;
|
||||
type Response = (Request, Framed<T, Codec>);
|
||||
type Error = ParseError;
|
||||
type InitError = ();
|
||||
type Service = OneRequestService<T>;
|
||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
ok(OneRequestService {
|
||||
_t: PhantomData,
|
||||
config: self.config.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `Service` implementation for HTTP1 transport. Reads one request and returns
|
||||
/// request and framed object.
|
||||
pub struct OneRequestService<T> {
|
||||
_t: PhantomData<T>,
|
||||
config: ServiceConfig,
|
||||
}
|
||||
|
||||
impl<T> Service for OneRequestService<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Request = T;
|
||||
type Response = (Request, Framed<T, Codec>);
|
||||
type Error = ParseError;
|
||||
type Future = OneRequestServiceResponse<T>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Self::Request) -> Self::Future {
|
||||
OneRequestServiceResponse {
|
||||
framed: Some(Framed::new(req, Codec::new(self.config.clone()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct OneRequestServiceResponse<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
framed: Option<Framed<T, Codec>>,
|
||||
}
|
||||
|
||||
impl<T> Future for OneRequestServiceResponse<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Output = Result<(Request, Framed<T, Codec>), ParseError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.framed.as_mut().unwrap().next_item(cx) {
|
||||
Poll::Ready(Some(Ok(req))) => match req {
|
||||
Message::Item(req) => {
|
||||
Poll::Ready(Ok((req, self.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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,20 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use futures_util::future::Ready;
|
||||
use futures_util::future::{ready, Ready};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::h1::Codec;
|
||||
use crate::request::Request;
|
||||
|
||||
pub struct UpgradeHandler<T>(PhantomData<T>);
|
||||
pub struct UpgradeHandler;
|
||||
|
||||
impl<T> ServiceFactory for UpgradeHandler<T> {
|
||||
type Config = ();
|
||||
type Request = (Request, Framed<T, Codec>);
|
||||
impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Service = UpgradeHandler<T>;
|
||||
type Config = ();
|
||||
type Service = UpgradeHandler;
|
||||
type InitError = Error;
|
||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
||||
|
||||
@ -25,8 +23,7 @@ impl<T> ServiceFactory for UpgradeHandler<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Service for UpgradeHandler<T> {
|
||||
type Request = (Request, Framed<T, Codec>);
|
||||
impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler {
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
@ -35,7 +32,7 @@ impl<T> Service for UpgradeHandler<T> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, _: Self::Request) -> Self::Future {
|
||||
unimplemented!()
|
||||
fn call(&mut self, _: (Request, Framed<T, Codec>)) -> Self::Future {
|
||||
ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -1,63 +1,66 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::net;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{cmp, convert::TryFrom};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::time::{Delay, Instant};
|
||||
use actix_rt::time::{Instant, Sleep};
|
||||
use actix_service::Service;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::ready;
|
||||
use h2::server::{Connection, SendResponse};
|
||||
use h2::SendStream;
|
||||
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
|
||||
use log::{error, trace};
|
||||
|
||||
use crate::body::{BodySize, MessageBody, ResponseBody};
|
||||
use crate::cloneable::CloneableService;
|
||||
use crate::config::ServiceConfig;
|
||||
use crate::error::{DispatchError, Error};
|
||||
use crate::helpers::DataFactory;
|
||||
use crate::httpmessage::HttpMessage;
|
||||
use crate::message::ResponseHead;
|
||||
use crate::payload::Payload;
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::service::HttpFlow;
|
||||
use crate::OnConnectData;
|
||||
|
||||
const CHUNK_SIZE: usize = 16_384;
|
||||
|
||||
/// Dispatcher for HTTP/2 protocol
|
||||
/// Dispatcher for HTTP/2 protocol.
|
||||
#[pin_project::pin_project]
|
||||
pub struct Dispatcher<T, S: Service<Request = Request>, B: MessageBody>
|
||||
pub struct Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request>,
|
||||
B: MessageBody,
|
||||
{
|
||||
service: CloneableService<S>,
|
||||
flow: Rc<RefCell<HttpFlow<S, X, U>>>,
|
||||
connection: Connection<T, Bytes>,
|
||||
on_connect: Option<Box<dyn DataFactory>>,
|
||||
on_connect_data: OnConnectData,
|
||||
config: ServiceConfig,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
ka_expire: Instant,
|
||||
ka_timer: Option<Delay>,
|
||||
_t: PhantomData<B>,
|
||||
ka_timer: Option<Sleep>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<T, S, B> Dispatcher<T, S, B>
|
||||
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error>,
|
||||
// S::Future: 'static,
|
||||
S::Response: Into<Response<B>>,
|
||||
B: MessageBody,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
service: CloneableService<S>,
|
||||
services: Rc<RefCell<HttpFlow<S, X, U>>>,
|
||||
connection: Connection<T, Bytes>,
|
||||
on_connect: Option<Box<dyn DataFactory>>,
|
||||
on_connect_data: OnConnectData,
|
||||
config: ServiceConfig,
|
||||
timeout: Option<Delay>,
|
||||
timeout: Option<Sleep>,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
) -> Self {
|
||||
// let keepalive = config.keep_alive_enabled();
|
||||
@ -77,22 +80,22 @@ where
|
||||
};
|
||||
|
||||
Dispatcher {
|
||||
service,
|
||||
flow: services,
|
||||
config,
|
||||
peer_addr,
|
||||
connection,
|
||||
on_connect,
|
||||
on_connect_data,
|
||||
ka_expire,
|
||||
ka_timer,
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B> Future for Dispatcher<T, S, B>
|
||||
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
@ -105,10 +108,12 @@ where
|
||||
let this = self.get_mut();
|
||||
|
||||
loop {
|
||||
match Pin::new(&mut this.connection).poll_accept(cx) {
|
||||
Poll::Ready(None) => return Poll::Ready(Ok(())),
|
||||
Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())),
|
||||
Poll::Ready(Some(Ok((req, res)))) => {
|
||||
match ready!(Pin::new(&mut this.connection).poll_accept(cx)) {
|
||||
None => return Poll::Ready(Ok(())),
|
||||
|
||||
Some(Err(err)) => return Poll::Ready(Err(err.into())),
|
||||
|
||||
Some(Ok((req, res))) => {
|
||||
// update keep-alive expire
|
||||
if this.ka_timer.is_some() {
|
||||
if let Some(expire) = this.config.keep_alive_expire() {
|
||||
@ -117,11 +122,9 @@ where
|
||||
}
|
||||
|
||||
let (parts, body) = req.into_parts();
|
||||
let mut req = Request::with_payload(Payload::<
|
||||
crate::payload::PayloadStream,
|
||||
>::H2(
|
||||
crate::h2::Payload::new(body)
|
||||
));
|
||||
let pl = crate::h2::Payload::new(body);
|
||||
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
|
||||
let mut req = Request::with_payload(pl);
|
||||
|
||||
let head = &mut req.head_mut();
|
||||
head.uri = parts.uri;
|
||||
@ -130,27 +133,21 @@ where
|
||||
head.headers = parts.headers.into();
|
||||
head.peer_addr = this.peer_addr;
|
||||
|
||||
// set on_connect data
|
||||
if let Some(ref on_connect) = this.on_connect {
|
||||
on_connect.set(&mut req.extensions_mut());
|
||||
}
|
||||
// merge on_connect_ext data into request extensions
|
||||
this.on_connect_data.merge_into(&mut req);
|
||||
|
||||
actix_rt::spawn(ServiceResponse::<
|
||||
S::Future,
|
||||
S::Response,
|
||||
S::Error,
|
||||
B,
|
||||
> {
|
||||
let svc = ServiceResponse::<S::Future, S::Response, S::Error, B> {
|
||||
state: ServiceResponseState::ServiceCall(
|
||||
this.service.call(req),
|
||||
this.flow.borrow_mut().service.call(req),
|
||||
Some(res),
|
||||
),
|
||||
config: this.config.clone(),
|
||||
buffer: None,
|
||||
_t: PhantomData,
|
||||
});
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
|
||||
actix_rt::spawn(svc);
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,7 +159,7 @@ struct ServiceResponse<F, I, E, B> {
|
||||
state: ServiceResponseState<F, B>,
|
||||
config: ServiceConfig,
|
||||
buffer: Option<Bytes>,
|
||||
_t: PhantomData<(I, E)>,
|
||||
_phantom: PhantomData<(I, E)>,
|
||||
}
|
||||
|
||||
#[pin_project::pin_project(project = ServiceResponseStateProj)]
|
||||
@ -199,8 +196,9 @@ where
|
||||
skip_len = true;
|
||||
*size = BodySize::Stream;
|
||||
}
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = match size {
|
||||
BodySize::None | BodySize::Stream => None,
|
||||
BodySize::Empty => res
|
||||
@ -215,11 +213,13 @@ where
|
||||
// copy headers
|
||||
for (key, value) in head.headers.iter() {
|
||||
match *key {
|
||||
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
|
||||
// omit HTTP/1 only headers
|
||||
CONNECTION | TRANSFER_ENCODING => continue,
|
||||
CONTENT_LENGTH if skip_len => continue,
|
||||
DATE => has_date = true,
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
res.headers_mut().append(key, value.clone());
|
||||
}
|
||||
|
||||
@ -227,9 +227,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
|
||||
@ -249,109 +251,121 @@ where
|
||||
let mut this = self.as_mut().project();
|
||||
|
||||
match this.state.project() {
|
||||
ServiceResponseStateProj::ServiceCall(call, send) => match call.poll(cx) {
|
||||
Poll::Ready(Ok(res)) => {
|
||||
let (res, body) = res.into().replace_body(());
|
||||
ServiceResponseStateProj::ServiceCall(call, send) => {
|
||||
match ready!(call.poll(cx)) {
|
||||
Ok(res) => {
|
||||
let (res, body) = res.into().replace_body(());
|
||||
|
||||
let mut send = send.take().unwrap();
|
||||
let mut size = body.size();
|
||||
let h2_res = self.as_mut().prepare_response(res.head(), &mut size);
|
||||
this = self.as_mut().project();
|
||||
let mut send = send.take().unwrap();
|
||||
let mut size = body.size();
|
||||
let h2_res =
|
||||
self.as_mut().prepare_response(res.head(), &mut size);
|
||||
this = self.as_mut().project();
|
||||
|
||||
let stream = match send.send_response(h2_res, size.is_eof()) {
|
||||
Err(e) => {
|
||||
trace!("Error sending h2 response: {:?}", e);
|
||||
return Poll::Ready(());
|
||||
let stream = match send.send_response(h2_res, size.is_eof()) {
|
||||
Err(e) => {
|
||||
trace!("Error sending HTTP/2 response: {:?}", e);
|
||||
return Poll::Ready(());
|
||||
}
|
||||
Ok(stream) => stream,
|
||||
};
|
||||
|
||||
if size.is_eof() {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
this.state
|
||||
.set(ServiceResponseState::SendPayload(stream, body));
|
||||
self.poll(cx)
|
||||
}
|
||||
Ok(stream) => stream,
|
||||
};
|
||||
}
|
||||
|
||||
if size.is_eof() {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
this.state
|
||||
.set(ServiceResponseState::SendPayload(stream, body));
|
||||
self.poll(cx)
|
||||
Err(e) => {
|
||||
let res: Response = e.into().into();
|
||||
let (res, body) = res.replace_body(());
|
||||
|
||||
let mut send = send.take().unwrap();
|
||||
let mut size = body.size();
|
||||
let h2_res =
|
||||
self.as_mut().prepare_response(res.head(), &mut size);
|
||||
this = self.as_mut().project();
|
||||
|
||||
let stream = match send.send_response(h2_res, size.is_eof()) {
|
||||
Err(e) => {
|
||||
trace!("Error sending HTTP/2 response: {:?}", e);
|
||||
return Poll::Ready(());
|
||||
}
|
||||
Ok(stream) => stream,
|
||||
};
|
||||
|
||||
if size.is_eof() {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
this.state.set(ServiceResponseState::SendPayload(
|
||||
stream,
|
||||
body.into_body(),
|
||||
));
|
||||
self.poll(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Err(e)) => {
|
||||
let res: Response = e.into().into();
|
||||
let (res, body) = res.replace_body(());
|
||||
}
|
||||
|
||||
let mut send = send.take().unwrap();
|
||||
let mut size = body.size();
|
||||
let h2_res = self.as_mut().prepare_response(res.head(), &mut size);
|
||||
this = self.as_mut().project();
|
||||
|
||||
let stream = match send.send_response(h2_res, size.is_eof()) {
|
||||
Err(e) => {
|
||||
trace!("Error sending h2 response: {:?}", e);
|
||||
return Poll::Ready(());
|
||||
}
|
||||
Ok(stream) => stream,
|
||||
};
|
||||
|
||||
if size.is_eof() {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
this.state.set(ServiceResponseState::SendPayload(
|
||||
stream,
|
||||
body.into_body(),
|
||||
));
|
||||
self.poll(cx)
|
||||
}
|
||||
}
|
||||
},
|
||||
ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => loop {
|
||||
ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => {
|
||||
loop {
|
||||
if let Some(ref mut buffer) = this.buffer {
|
||||
match stream.poll_capacity(cx) {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(None) => return Poll::Ready(()),
|
||||
Poll::Ready(Some(Ok(cap))) => {
|
||||
let len = buffer.len();
|
||||
let bytes = buffer.split_to(std::cmp::min(cap, len));
|
||||
loop {
|
||||
match this.buffer {
|
||||
Some(ref mut buffer) => {
|
||||
match ready!(stream.poll_capacity(cx)) {
|
||||
None => return Poll::Ready(()),
|
||||
|
||||
if let Err(e) = stream.send_data(bytes, false) {
|
||||
warn!("{:?}", e);
|
||||
Some(Ok(cap)) => {
|
||||
let len = buffer.len();
|
||||
let bytes = buffer.split_to(cmp::min(cap, len));
|
||||
|
||||
if let Err(e) = stream.send_data(bytes, false) {
|
||||
warn!("{:?}", e);
|
||||
return Poll::Ready(());
|
||||
} else if !buffer.is_empty() {
|
||||
let cap = cmp::min(buffer.len(), CHUNK_SIZE);
|
||||
stream.reserve_capacity(cap);
|
||||
} else {
|
||||
this.buffer.take();
|
||||
}
|
||||
}
|
||||
|
||||
Some(Err(e)) => {
|
||||
warn!("{:?}", e);
|
||||
return Poll::Ready(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => match ready!(body.as_mut().poll_next(cx)) {
|
||||
None => {
|
||||
if let Err(e) = stream.send_data(Bytes::new(), true)
|
||||
{
|
||||
warn!("{:?}", e);
|
||||
}
|
||||
return Poll::Ready(());
|
||||
} else if !buffer.is_empty() {
|
||||
let cap = std::cmp::min(buffer.len(), CHUNK_SIZE);
|
||||
stream.reserve_capacity(cap);
|
||||
} else {
|
||||
this.buffer.take();
|
||||
}
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => {
|
||||
warn!("{:?}", e);
|
||||
return Poll::Ready(());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match body.as_mut().poll_next(cx) {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(None) => {
|
||||
if let Err(e) = stream.send_data(Bytes::new(), true) {
|
||||
warn!("{:?}", e);
|
||||
|
||||
Some(Ok(chunk)) => {
|
||||
stream.reserve_capacity(cmp::min(
|
||||
chunk.len(),
|
||||
CHUNK_SIZE,
|
||||
));
|
||||
*this.buffer = Some(chunk);
|
||||
}
|
||||
return Poll::Ready(());
|
||||
}
|
||||
Poll::Ready(Some(Ok(chunk))) => {
|
||||
stream.reserve_capacity(std::cmp::min(
|
||||
chunk.len(),
|
||||
CHUNK_SIZE,
|
||||
));
|
||||
*this.buffer = Some(chunk);
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => {
|
||||
error!("Response payload stream error: {:?}", e);
|
||||
return Poll::Ready(());
|
||||
}
|
||||
|
||||
Some(Err(e)) => {
|
||||
error!("Response payload stream error: {:?}", e);
|
||||
return Poll::Ready(());
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
//! HTTP/2 implementation
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
//! HTTP/2 implementation.
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use futures_core::{ready, Stream};
|
||||
use h2::RecvStream;
|
||||
|
||||
mod dispatcher;
|
||||
@ -13,14 +16,14 @@ pub use self::dispatcher::Dispatcher;
|
||||
pub use self::service::H2Service;
|
||||
use crate::error::PayloadError;
|
||||
|
||||
/// H2 receive stream
|
||||
/// HTTP/2 peer stream.
|
||||
pub struct Payload {
|
||||
pl: RecvStream,
|
||||
stream: RecvStream,
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
pub(crate) fn new(pl: RecvStream) -> Self {
|
||||
Self { pl }
|
||||
pub(crate) fn new(stream: RecvStream) -> Self {
|
||||
Self { stream }
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,18 +36,17 @@ impl Stream for Payload {
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
|
||||
match Pin::new(&mut this.pl).poll_data(cx) {
|
||||
Poll::Ready(Some(Ok(chunk))) => {
|
||||
match ready!(Pin::new(&mut this.stream).poll_data(cx)) {
|
||||
Some(Ok(chunk)) => {
|
||||
let len = chunk.len();
|
||||
if let Err(err) = this.pl.flow_control().release_capacity(len) {
|
||||
Poll::Ready(Some(Err(err.into())))
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(chunk)))
|
||||
|
||||
match this.stream.flow_control().release_capacity(len) {
|
||||
Ok(()) => Poll::Ready(Some(Ok(chunk))),
|
||||
Err(err) => Poll::Ready(Some(Err(err.into()))),
|
||||
}
|
||||
}
|
||||
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err.into()))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
use std::cell::RefCell;
|
||||
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;
|
||||
@ -17,68 +18,65 @@ use h2::server::{self, Handshake};
|
||||
use log::error;
|
||||
|
||||
use crate::body::MessageBody;
|
||||
use crate::cloneable::CloneableService;
|
||||
use crate::config::ServiceConfig;
|
||||
use crate::error::{DispatchError, Error};
|
||||
use crate::helpers::DataFactory;
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::service::HttpFlow;
|
||||
use crate::{ConnectCallback, OnConnectData};
|
||||
|
||||
use super::dispatcher::Dispatcher;
|
||||
|
||||
/// `ServiceFactory` implementation for HTTP2 transport
|
||||
/// `ServiceFactory` implementation for HTTP/2 transport
|
||||
pub struct H2Service<T, S, B> {
|
||||
srv: S,
|
||||
cfg: ServiceConfig,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<(T, B)>,
|
||||
}
|
||||
|
||||
impl<T, S, B> H2Service<T, S, B>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create new `HttpService` instance with config.
|
||||
pub(crate) fn with_config<F: IntoServiceFactory<S>>(
|
||||
/// Create new `H2Service` instance with config.
|
||||
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
|
||||
cfg: ServiceConfig,
|
||||
service: F,
|
||||
) -> Self {
|
||||
H2Service {
|
||||
cfg,
|
||||
on_connect: None,
|
||||
on_connect_ext: None,
|
||||
srv: service.into_factory(),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set on connect callback.
|
||||
pub(crate) fn on_connect(
|
||||
mut self,
|
||||
f: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
) -> Self {
|
||||
self.on_connect = f;
|
||||
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>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create simple tcp based service
|
||||
/// Create plain TCP based service
|
||||
pub fn tcp(
|
||||
self,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = DispatchError,
|
||||
InitError = S::InitError,
|
||||
@ -95,34 +93,34 @@ where
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
mod openssl {
|
||||
use actix_service::{fn_factory, fn_service};
|
||||
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
|
||||
use actix_tls::{openssl::HandshakeError, SslError};
|
||||
use actix_service::{fn_factory, fn_service, ServiceFactoryExt};
|
||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<S, B> H2Service<SslStream<TcpStream>, S, B>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create ssl based service
|
||||
/// Create OpenSSL based service
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<HandshakeError<TcpStream>, DispatchError>,
|
||||
Error = TlsError<SslError, 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 +129,7 @@ mod openssl {
|
||||
ok((io, peer_addr))
|
||||
}))
|
||||
}))
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,27 +137,28 @@ mod openssl {
|
||||
#[cfg(feature = "rustls")]
|
||||
mod rustls {
|
||||
use super::*;
|
||||
use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
|
||||
use actix_tls::SslError;
|
||||
use actix_service::ServiceFactoryExt;
|
||||
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
use std::io;
|
||||
|
||||
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create openssl based service
|
||||
/// Create Rustls based service
|
||||
pub fn rustls(
|
||||
self,
|
||||
mut config: ServerConfig,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<io::Error, DispatchError>,
|
||||
Error = TlsError<io::Error, DispatchError>,
|
||||
InitError = S::InitError,
|
||||
> {
|
||||
let protos = vec!["h2".to_string().into()];
|
||||
@ -167,7 +166,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,55 +175,57 @@ mod rustls {
|
||||
ok((io, peer_addr))
|
||||
}))
|
||||
}))
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B> ServiceFactory for H2Service<T, S, B>
|
||||
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
type Config = ();
|
||||
type Request = (T, Option<net::SocketAddr>);
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
type InitError = S::InitError;
|
||||
type Config = ();
|
||||
type Service = H2ServiceHandler<T, S::Service, B>;
|
||||
type InitError = S::InitError;
|
||||
type Future = H2ServiceResponse<T, S, B>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
H2ServiceResponse {
|
||||
fut: self.srv.new_service(()),
|
||||
cfg: Some(self.cfg.clone()),
|
||||
on_connect: self.on_connect.clone(),
|
||||
_t: PhantomData,
|
||||
on_connect_ext: self.on_connect_ext.clone(),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[pin_project::pin_project]
|
||||
pub struct H2ServiceResponse<T, S: ServiceFactory, B> {
|
||||
pub struct H2ServiceResponse<T, S, B>
|
||||
where
|
||||
S: ServiceFactory<Request>,
|
||||
{
|
||||
#[pin]
|
||||
fut: S::Future,
|
||||
cfg: Option<ServiceConfig>,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<T, S, B> Future for H2ServiceResponse<T, S, B>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
type Output = Result<H2ServiceHandler<T, S::Service, B>, S::InitError>;
|
||||
@ -232,28 +233,31 @@ where
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.as_mut().project();
|
||||
|
||||
Poll::Ready(ready!(this.fut.poll(cx)).map(|service| {
|
||||
this.fut.poll(cx).map_ok(|service| {
|
||||
let this = self.as_mut().project();
|
||||
H2ServiceHandler::new(
|
||||
this.cfg.take().unwrap(),
|
||||
this.on_connect.clone(),
|
||||
this.on_connect_ext.clone(),
|
||||
service,
|
||||
)
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `Service` implementation for http/2 transport
|
||||
pub struct H2ServiceHandler<T, S: Service, B> {
|
||||
srv: CloneableService<S>,
|
||||
pub struct H2ServiceHandler<T, S, B>
|
||||
where
|
||||
S: Service<Request>,
|
||||
{
|
||||
flow: Rc<RefCell<HttpFlow<S, (), ()>>>,
|
||||
cfg: ServiceConfig,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<T, S, B> H2ServiceHandler<T, S, B>
|
||||
where
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
@ -261,70 +265,66 @@ where
|
||||
{
|
||||
fn new(
|
||||
cfg: ServiceConfig,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
srv: S,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
service: S,
|
||||
) -> H2ServiceHandler<T, S, B> {
|
||||
H2ServiceHandler {
|
||||
flow: HttpFlow::new(service, (), None),
|
||||
cfg,
|
||||
on_connect,
|
||||
srv: CloneableService::new(srv),
|
||||
_t: PhantomData,
|
||||
on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B> Service for H2ServiceHandler<T, S, B>
|
||||
impl<T, S, B> Service<(T, Option<net::SocketAddr>)> for H2ServiceHandler<T, S, B>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
type Request = (T, Option<net::SocketAddr>);
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
type Future = H2ServiceHandlerResponse<T, S, B>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.srv.poll_ready(cx).map_err(|e| {
|
||||
self.flow.borrow_mut().service.poll_ready(cx).map_err(|e| {
|
||||
let e = e.into();
|
||||
error!("Service readiness error: {:?}", e);
|
||||
DispatchError::Service(e)
|
||||
})
|
||||
}
|
||||
|
||||
fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
|
||||
let on_connect = if let Some(ref on_connect) = self.on_connect {
|
||||
Some(on_connect(&io))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
fn call(&mut self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||
let on_connect_data =
|
||||
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||
|
||||
H2ServiceHandlerResponse {
|
||||
state: State::Handshake(
|
||||
Some(self.srv.clone()),
|
||||
Some(self.flow.clone()),
|
||||
Some(self.cfg.clone()),
|
||||
addr,
|
||||
on_connect,
|
||||
on_connect_data,
|
||||
server::handshake(io),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum State<T, S: Service<Request = Request>, B: MessageBody>
|
||||
enum State<T, S: Service<Request>, B: MessageBody>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S::Future: 'static,
|
||||
{
|
||||
Incoming(Dispatcher<T, S, B>),
|
||||
Incoming(Dispatcher<T, S, B, (), ()>),
|
||||
Handshake(
|
||||
Option<CloneableService<S>>,
|
||||
Option<Rc<RefCell<HttpFlow<S, (), ()>>>>,
|
||||
Option<ServiceConfig>,
|
||||
Option<net::SocketAddr>,
|
||||
Option<Box<dyn DataFactory>>,
|
||||
OnConnectData,
|
||||
Handshake<T, Bytes>,
|
||||
),
|
||||
}
|
||||
@ -332,7 +332,7 @@ where
|
||||
pub struct H2ServiceHandlerResponse<T, S, B>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
@ -344,7 +344,7 @@ where
|
||||
impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
@ -359,25 +359,25 @@ where
|
||||
ref mut srv,
|
||||
ref mut config,
|
||||
ref peer_addr,
|
||||
ref mut on_connect,
|
||||
ref mut on_connect_data,
|
||||
ref mut handshake,
|
||||
) => match Pin::new(handshake).poll(cx) {
|
||||
Poll::Ready(Ok(conn)) => {
|
||||
) => match ready!(Pin::new(handshake).poll(cx)) {
|
||||
Ok(conn) => {
|
||||
let on_connect_data = std::mem::take(on_connect_data);
|
||||
self.state = State::Incoming(Dispatcher::new(
|
||||
srv.take().unwrap(),
|
||||
conn,
|
||||
on_connect.take(),
|
||||
on_connect_data,
|
||||
config.take().unwrap(),
|
||||
None,
|
||||
*peer_addr,
|
||||
));
|
||||
self.poll(cx)
|
||||
}
|
||||
Poll::Ready(Err(err)) => {
|
||||
Err(err) => {
|
||||
trace!("H2 handshake error: {}", err);
|
||||
Poll::Ready(Err(err.into()))
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use mime::Mime;
|
||||
|
||||
use crate::header::{qitem, QualityItem};
|
||||
@ -7,7 +9,7 @@ header! {
|
||||
/// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2)
|
||||
///
|
||||
/// The `Accept` header field can be used by user agents to specify
|
||||
/// response media types that are acceptable. Accept header fields can
|
||||
/// response media types that are acceptable. Accept header fields can
|
||||
/// be used to indicate that the request is specifically limited to a
|
||||
/// small set of desired types, as in the case of a request for an
|
||||
/// in-line image
|
||||
@ -97,14 +99,14 @@ header! {
|
||||
test_header!(
|
||||
test1,
|
||||
vec![b"audio/*; q=0.2, audio/basic"],
|
||||
Some(HeaderField(vec![
|
||||
Some(Accept(vec![
|
||||
QualityItem::new("audio/*".parse().unwrap(), q(200)),
|
||||
qitem("audio/basic".parse().unwrap()),
|
||||
])));
|
||||
test_header!(
|
||||
test2,
|
||||
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
|
||||
Some(HeaderField(vec![
|
||||
Some(Accept(vec![
|
||||
QualityItem::new(mime::TEXT_PLAIN, q(500)),
|
||||
qitem(mime::TEXT_HTML),
|
||||
QualityItem::new(
|
||||
@ -138,23 +140,148 @@ header! {
|
||||
}
|
||||
|
||||
impl Accept {
|
||||
/// A constructor to easily create `Accept: */*`.
|
||||
/// Construct `Accept: */*`.
|
||||
pub fn star() -> Accept {
|
||||
Accept(vec![qitem(mime::STAR_STAR)])
|
||||
}
|
||||
|
||||
/// A constructor to easily create `Accept: application/json`.
|
||||
/// Construct `Accept: application/json`.
|
||||
pub fn json() -> Accept {
|
||||
Accept(vec![qitem(mime::APPLICATION_JSON)])
|
||||
}
|
||||
|
||||
/// A constructor to easily create `Accept: text/*`.
|
||||
/// Construct `Accept: text/*`.
|
||||
pub fn text() -> Accept {
|
||||
Accept(vec![qitem(mime::TEXT_STAR)])
|
||||
}
|
||||
|
||||
/// A constructor to easily create `Accept: image/*`.
|
||||
/// Construct `Accept: image/*`.
|
||||
pub fn image() -> Accept {
|
||||
Accept(vec![qitem(mime::IMAGE_STAR)])
|
||||
}
|
||||
|
||||
/// Construct `Accept: text/html`.
|
||||
pub fn html() -> Accept {
|
||||
Accept(vec![qitem(mime::TEXT_HTML)])
|
||||
}
|
||||
|
||||
/// Returns a sorted list of mime types from highest to lowest preference, accounting for
|
||||
/// [q-factor weighting] and specificity.
|
||||
///
|
||||
/// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2
|
||||
pub fn mime_precedence(&self) -> Vec<Mime> {
|
||||
let mut types = self.0.clone();
|
||||
|
||||
// use stable sort so items with equal q-factor and specificity retain listed order
|
||||
types.sort_by(|a, b| {
|
||||
// sort by q-factor descending
|
||||
b.quality.cmp(&a.quality).then_with(|| {
|
||||
// use specificity rules on mime types with
|
||||
// same q-factor (eg. text/html > text/* > */*)
|
||||
|
||||
// subtypes are not comparable if main type is star, so return
|
||||
match (a.item.type_(), b.item.type_()) {
|
||||
(mime::STAR, mime::STAR) => return Ordering::Equal,
|
||||
|
||||
// a is sorted after b
|
||||
(mime::STAR, _) => return Ordering::Greater,
|
||||
|
||||
// a is sorted before b
|
||||
(_, mime::STAR) => return Ordering::Less,
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// in both these match expressions, the returned ordering appears
|
||||
// inverted because sort is high-to-low ("descending") precedence
|
||||
match (a.item.subtype(), b.item.subtype()) {
|
||||
(mime::STAR, mime::STAR) => Ordering::Equal,
|
||||
|
||||
// a is sorted after b
|
||||
(mime::STAR, _) => Ordering::Greater,
|
||||
|
||||
// a is sorted before b
|
||||
(_, mime::STAR) => Ordering::Less,
|
||||
|
||||
_ => Ordering::Equal,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
types.into_iter().map(|qitem| qitem.item).collect()
|
||||
}
|
||||
|
||||
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first mime type is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// Returns `None` if contained list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2
|
||||
pub fn mime_preference(&self) -> Option<Mime> {
|
||||
let types = self.mime_precedence();
|
||||
types.first().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::header::q;
|
||||
|
||||
#[test]
|
||||
fn test_mime_precedence() {
|
||||
let test = Accept(vec![]);
|
||||
assert!(test.mime_precedence().is_empty());
|
||||
|
||||
let test = Accept(vec![qitem(mime::APPLICATION_JSON)]);
|
||||
assert_eq!(test.mime_precedence(), vec!(mime::APPLICATION_JSON));
|
||||
|
||||
let test = Accept(vec![
|
||||
qitem(mime::TEXT_HTML),
|
||||
"application/xhtml+xml".parse().unwrap(),
|
||||
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
|
||||
QualityItem::new(mime::STAR_STAR, q(0.8)),
|
||||
]);
|
||||
assert_eq!(
|
||||
test.mime_precedence(),
|
||||
vec![
|
||||
mime::TEXT_HTML,
|
||||
"application/xhtml+xml".parse().unwrap(),
|
||||
"application/xml".parse().unwrap(),
|
||||
mime::STAR_STAR,
|
||||
]
|
||||
);
|
||||
|
||||
let test = Accept(vec![
|
||||
qitem(mime::STAR_STAR),
|
||||
qitem(mime::IMAGE_STAR),
|
||||
qitem(mime::IMAGE_PNG),
|
||||
]);
|
||||
assert_eq!(
|
||||
test.mime_precedence(),
|
||||
vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mime_preference() {
|
||||
let test = Accept(vec![
|
||||
qitem(mime::TEXT_HTML),
|
||||
"application/xhtml+xml".parse().unwrap(),
|
||||
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
|
||||
QualityItem::new(mime::STAR_STAR, q(0.8)),
|
||||
]);
|
||||
assert_eq!(test.mime_preference(), Some(mime::TEXT_HTML));
|
||||
|
||||
let test = Accept(vec![
|
||||
QualityItem::new("video/*".parse().unwrap(), q(0.8)),
|
||||
qitem(mime::IMAGE_PNG),
|
||||
QualityItem::new(mime::STAR_STAR, q(0.5)),
|
||||
qitem(mime::IMAGE_SVG),
|
||||
QualityItem::new(mime::IMAGE_STAR, q(0.8)),
|
||||
]);
|
||||
assert_eq!(test.mime_preference(), Some(mime::IMAGE_PNG));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use http::Method;
|
||||
use http::header;
|
||||
use http::Method;
|
||||
|
||||
header! {
|
||||
/// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1)
|
||||
|
@ -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
|
||||
@ -318,9 +318,8 @@ impl ContentDisposition {
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
left = new_left;
|
||||
if param_name.ends_with('*') {
|
||||
if let Some(param_name) = param_name.strip_suffix('*') {
|
||||
// extended parameters
|
||||
let param_name = ¶m_name[..param_name.len() - 1]; // trim asterisk
|
||||
let (ext_value, new_left) = split_once_and_trim(left, ';');
|
||||
left = new_left;
|
||||
let ext_value = header::parse_extended_value(ext_value)?;
|
||||
@ -387,26 +386,17 @@ impl ContentDisposition {
|
||||
|
||||
/// Returns `true` if it is [`Inline`](DispositionType::Inline).
|
||||
pub fn is_inline(&self) -> bool {
|
||||
match self.disposition {
|
||||
DispositionType::Inline => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self.disposition, DispositionType::Inline)
|
||||
}
|
||||
|
||||
/// Returns `true` if it is [`Attachment`](DispositionType::Attachment).
|
||||
pub fn is_attachment(&self) -> bool {
|
||||
match self.disposition {
|
||||
DispositionType::Attachment => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self.disposition, DispositionType::Attachment)
|
||||
}
|
||||
|
||||
/// Returns `true` if it is [`FormData`](DispositionType::FormData).
|
||||
pub fn is_form_data(&self) -> bool {
|
||||
match self.disposition {
|
||||
DispositionType::FormData => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self.disposition, DispositionType::FormData)
|
||||
}
|
||||
|
||||
/// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches.
|
||||
@ -559,8 +549,7 @@ impl fmt::Display for ContentDisposition {
|
||||
write!(f, "{}", self.disposition)?;
|
||||
self.parameters
|
||||
.iter()
|
||||
.map(|param| write!(f, "; {}", param))
|
||||
.collect()
|
||||
.try_for_each(|param| write!(f, "; {}", param))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,17 +3,19 @@
|
||||
//! ## Mime
|
||||
//!
|
||||
//! Several header fields use MIME values for their contents. Keeping with the
|
||||
//! strongly-typed theme, the [mime](https://docs.rs/mime) crate
|
||||
//! strongly-typed theme, the [mime] crate
|
||||
//! is used, such as `ContentType(pub Mime)`.
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
|
||||
pub use self::accept_charset::AcceptCharset;
|
||||
//pub use self::accept_encoding::AcceptEncoding;
|
||||
pub use self::accept_language::AcceptLanguage;
|
||||
pub use self::accept::Accept;
|
||||
pub use self::accept_language::AcceptLanguage;
|
||||
pub use self::allow::Allow;
|
||||
pub use self::cache_control::{CacheControl, CacheDirective};
|
||||
pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam};
|
||||
pub use self::content_disposition::{
|
||||
ContentDisposition, DispositionParam, DispositionType,
|
||||
};
|
||||
pub use self::content_language::ContentLanguage;
|
||||
pub use self::content_range::{ContentRange, ContentRangeSpec};
|
||||
pub use self::content_type::ContentType;
|
||||
@ -47,7 +49,7 @@ macro_rules! __hyper__deref {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@ -74,8 +76,8 @@ macro_rules! test_header {
|
||||
($id:ident, $raw:expr) => {
|
||||
#[test]
|
||||
fn $id() {
|
||||
use $crate::test;
|
||||
use super::*;
|
||||
use $crate::test;
|
||||
|
||||
let raw = $raw;
|
||||
let a: Vec<Vec<u8>> = raw.iter().map(|x| x.to_vec()).collect();
|
||||
@ -118,7 +120,7 @@ macro_rules! test_header {
|
||||
// Test formatting
|
||||
if typed.is_some() {
|
||||
let raw = &($raw)[..];
|
||||
let mut iter = raw.iter().map(|b|str::from_utf8(&b[..]).unwrap());
|
||||
let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap());
|
||||
let mut joined = String::new();
|
||||
joined.push_str(iter.next().unwrap());
|
||||
for s in iter {
|
||||
@ -128,7 +130,7 @@ macro_rules! test_header {
|
||||
assert_eq!(format!("{}", typed.unwrap()), joined);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
@ -330,11 +332,10 @@ macro_rules! header {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
mod accept_charset;
|
||||
//mod accept_encoding;
|
||||
mod accept_language;
|
||||
mod accept;
|
||||
mod accept_language;
|
||||
mod allow;
|
||||
mod cache_control;
|
||||
mod content_disposition;
|
||||
|
@ -8,8 +8,6 @@ use http::header::{HeaderName, HeaderValue};
|
||||
/// A set of HTTP headers
|
||||
///
|
||||
/// `HeaderMap` is an multi-map of [`HeaderName`] to values.
|
||||
///
|
||||
/// [`HeaderName`]: struct.HeaderName.html
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HeaderMap {
|
||||
pub(crate) inner: FxHashMap<HeaderName, Value>,
|
||||
@ -141,8 +139,6 @@ impl HeaderMap {
|
||||
/// The returned view does not incur any allocations and allows iterating
|
||||
/// the values associated with the key. See [`GetAll`] for more details.
|
||||
/// Returns `None` if there are no values associated with the key.
|
||||
///
|
||||
/// [`GetAll`]: struct.GetAll.html
|
||||
pub fn get_all<N: AsName>(&self, name: N) -> GetAll<'_> {
|
||||
GetAll {
|
||||
idx: 0,
|
||||
|
@ -148,10 +148,7 @@ impl ContentEncoding {
|
||||
#[inline]
|
||||
/// Is the content compressed?
|
||||
pub fn is_compression(self) -> bool {
|
||||
match self {
|
||||
ContentEncoding::Identity | ContentEncoding::Auto => false,
|
||||
_ => true,
|
||||
}
|
||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -373,9 +370,7 @@ impl fmt::Display for ExtendedValue {
|
||||
}
|
||||
|
||||
/// Percent encode a sequence of bytes with a character set defined in
|
||||
/// [https://tools.ietf.org/html/rfc5987#section-3.2][url]
|
||||
///
|
||||
/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2
|
||||
/// <https://tools.ietf.org/html/rfc5987#section-3.2>
|
||||
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
|
||||
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
||||
fmt::Display::fmt(&encoded, f)
|
||||
|
@ -7,9 +7,7 @@ use self::Charset::*;
|
||||
///
|
||||
/// The string representation is normalized to upper case.
|
||||
///
|
||||
/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url].
|
||||
///
|
||||
/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml
|
||||
/// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum Charset {
|
||||
|
@ -7,10 +7,12 @@ use crate::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer};
|
||||
/// 1. `%x21`, or
|
||||
/// 2. in the range `%x23` to `%x7E`, or
|
||||
/// 3. above `%x80`
|
||||
fn entity_validate_char(c: u8) -> bool {
|
||||
c == 0x21 || (0x23..=0x7e).contains(&c) || (c >= 0x80)
|
||||
}
|
||||
|
||||
fn check_slice_validity(slice: &str) -> bool {
|
||||
slice
|
||||
.bytes()
|
||||
.all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80'))
|
||||
slice.bytes().all(entity_validate_char)
|
||||
}
|
||||
|
||||
/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3)
|
||||
|
@ -3,7 +3,8 @@ use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use bytes::{buf::BufMutExt, BytesMut};
|
||||
use bytes::buf::BufMut;
|
||||
use bytes::BytesMut;
|
||||
use http::header::{HeaderValue, InvalidHeaderValue};
|
||||
use time::{offset, OffsetDateTime, PrimitiveDateTime};
|
||||
|
||||
|
@ -1,10 +1,17 @@
|
||||
use std::{cmp, fmt, str};
|
||||
use std::{
|
||||
cmp,
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt, str,
|
||||
};
|
||||
|
||||
use self::internal::IntoQuality;
|
||||
use derive_more::{Display, Error};
|
||||
|
||||
const MAX_QUALITY: u16 = 1000;
|
||||
const MAX_FLOAT_QUALITY: f32 = 1.0;
|
||||
|
||||
/// Represents a quality used in quality values.
|
||||
///
|
||||
/// Can be created with the `q` function.
|
||||
/// Can be created with the [`q`] function.
|
||||
///
|
||||
/// # Implementation notes
|
||||
///
|
||||
@ -18,12 +25,54 @@ use self::internal::IntoQuality;
|
||||
///
|
||||
/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1)
|
||||
/// gives more information on quality values in HTTP header fields.
|
||||
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Quality(u16);
|
||||
|
||||
impl Quality {
|
||||
/// # Panics
|
||||
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
|
||||
fn from_f32(value: f32) -> Self {
|
||||
// Check that `value` is within range should be done before calling this method.
|
||||
// Just in case, this debug_assert should catch if we were forgetful.
|
||||
debug_assert!(
|
||||
(0.0f32..=1.0f32).contains(&value),
|
||||
"q value must be between 0.0 and 1.0"
|
||||
);
|
||||
|
||||
Quality((value * MAX_QUALITY as f32) as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Quality {
|
||||
fn default() -> Quality {
|
||||
Quality(1000)
|
||||
Quality(MAX_QUALITY)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
pub struct QualityOutOfBounds;
|
||||
|
||||
impl TryFrom<u16> for Quality {
|
||||
type Error = QualityOutOfBounds;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
if (0..=MAX_QUALITY).contains(&value) {
|
||||
Ok(Quality(value))
|
||||
} else {
|
||||
Err(QualityOutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<f32> for Quality {
|
||||
type Error = QualityOutOfBounds;
|
||||
|
||||
fn try_from(value: f32) -> Result<Self, Self::Error> {
|
||||
if (0.0..=MAX_FLOAT_QUALITY).contains(&value) {
|
||||
Ok(Quality::from_f32(value))
|
||||
} else {
|
||||
Err(QualityOutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,8 +104,9 @@ impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> {
|
||||
impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.item, f)?;
|
||||
|
||||
match self.quality.0 {
|
||||
1000 => Ok(()),
|
||||
MAX_QUALITY => Ok(()),
|
||||
0 => f.write_str("; q=0"),
|
||||
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')),
|
||||
}
|
||||
@ -66,105 +116,79 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
||||
impl<T: str::FromStr> str::FromStr for QualityItem<T> {
|
||||
type Err = crate::error::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<QualityItem<T>, crate::error::ParseError> {
|
||||
if !s.is_ascii() {
|
||||
fn from_str(qitem_str: &str) -> Result<QualityItem<T>, crate::error::ParseError> {
|
||||
if !qitem_str.is_ascii() {
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
|
||||
// Set defaults used if parsing fails.
|
||||
let mut raw_item = s;
|
||||
let mut raw_item = qitem_str;
|
||||
let mut quality = 1f32;
|
||||
|
||||
let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect();
|
||||
let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect();
|
||||
|
||||
if parts.len() == 2 {
|
||||
// example for item with q-factor:
|
||||
//
|
||||
// gzip; q=0.65
|
||||
// ^^^^^^ parts[0]
|
||||
// ^^ start
|
||||
// ^^^^ q_val
|
||||
// ^^^^ parts[1]
|
||||
|
||||
if parts[0].len() < 2 {
|
||||
// Can't possibly be an attribute since an attribute needs at least a name followed
|
||||
// by an equals sign. And bare identifiers are forbidden.
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
|
||||
let start = &parts[0][0..2];
|
||||
|
||||
if start == "q=" || start == "Q=" {
|
||||
let q_part = &parts[0][2..parts[0].len()];
|
||||
if q_part.len() > 5 {
|
||||
let q_val = &parts[0][2..];
|
||||
if q_val.len() > 5 {
|
||||
// longer than 5 indicates an over-precise q-factor
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
match q_part.parse::<f32>() {
|
||||
Ok(q_value) => {
|
||||
if 0f32 <= q_value && q_value <= 1f32 {
|
||||
quality = q_value;
|
||||
raw_item = parts[1];
|
||||
} else {
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
}
|
||||
Err(_) => return Err(crate::error::ParseError::Header),
|
||||
|
||||
let q_value = q_val
|
||||
.parse::<f32>()
|
||||
.map_err(|_| crate::error::ParseError::Header)?;
|
||||
|
||||
if (0f32..=1f32).contains(&q_value) {
|
||||
quality = q_value;
|
||||
raw_item = parts[1];
|
||||
} else {
|
||||
return Err(crate::error::ParseError::Header);
|
||||
}
|
||||
}
|
||||
}
|
||||
match raw_item.parse::<T>() {
|
||||
// we already checked above that the quality is within range
|
||||
Ok(item) => Ok(QualityItem::new(item, from_f32(quality))),
|
||||
Err(_) => Err(crate::error::ParseError::Header),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_f32(f: f32) -> Quality {
|
||||
// this function is only used internally. A check that `f` is within range
|
||||
// should be done before calling this method. Just in case, this
|
||||
// debug_assert should catch if we were forgetful
|
||||
debug_assert!(
|
||||
f >= 0f32 && f <= 1f32,
|
||||
"q value must be between 0.0 and 1.0"
|
||||
);
|
||||
Quality((f * 1000f32) as u16)
|
||||
let item = raw_item
|
||||
.parse::<T>()
|
||||
.map_err(|_| crate::error::ParseError::Header)?;
|
||||
|
||||
// we already checked above that the quality is within range
|
||||
Ok(QualityItem::new(item, Quality::from_f32(quality)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to wrap a value in a `QualityItem`
|
||||
/// Sets `q` to the default 1.0
|
||||
pub fn qitem<T>(item: T) -> QualityItem<T> {
|
||||
QualityItem::new(item, Default::default())
|
||||
QualityItem::new(item, Quality::default())
|
||||
}
|
||||
|
||||
/// Convenience function to create a `Quality` from a float or integer.
|
||||
///
|
||||
/// Implemented for `u16` and `f32`. Panics if value is out of range.
|
||||
pub fn q<T: IntoQuality>(val: T) -> Quality {
|
||||
val.into_quality()
|
||||
}
|
||||
|
||||
mod internal {
|
||||
use super::Quality;
|
||||
|
||||
// TryFrom is probably better, but it's not stable. For now, we want to
|
||||
// keep the functionality of the `q` function, while allowing it to be
|
||||
// generic over `f32` and `u16`.
|
||||
//
|
||||
// `q` would panic before, so keep that behavior. `TryFrom` can be
|
||||
// introduced later for a non-panicking conversion.
|
||||
|
||||
pub trait IntoQuality: Sealed + Sized {
|
||||
fn into_quality(self) -> Quality;
|
||||
}
|
||||
|
||||
impl IntoQuality for f32 {
|
||||
fn into_quality(self) -> Quality {
|
||||
assert!(
|
||||
self >= 0f32 && self <= 1f32,
|
||||
"float must be between 0.0 and 1.0"
|
||||
);
|
||||
super::from_f32(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuality for u16 {
|
||||
fn into_quality(self) -> Quality {
|
||||
assert!(self <= 1000, "u16 must be between 0 and 1000");
|
||||
Quality(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Sealed {}
|
||||
impl Sealed for u16 {}
|
||||
impl Sealed for f32 {}
|
||||
pub fn q<T>(val: T) -> Quality
|
||||
where
|
||||
T: TryInto<Quality>,
|
||||
T::Error: fmt::Debug,
|
||||
{
|
||||
// TODO: on next breaking change, handle unwrap differently
|
||||
val.try_into().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -270,15 +294,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic] // FIXME - 32-bit msvc unwinding broken
|
||||
#[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
|
||||
#[should_panic]
|
||||
fn test_quality_invalid() {
|
||||
q(-1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic] // FIXME - 32-bit msvc unwinding broken
|
||||
#[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
|
||||
#[should_panic]
|
||||
fn test_quality_invalid2() {
|
||||
q(2.0);
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ use std::io;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use http::Version;
|
||||
|
||||
use crate::extensions::Extensions;
|
||||
|
||||
const DIGITS_START: u8 = b'0';
|
||||
|
||||
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
|
||||
@ -50,23 +48,12 @@ impl<'a> io::Write for Writer<'a> {
|
||||
self.0.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait DataFactory {
|
||||
fn set(&self, ext: &mut Extensions);
|
||||
}
|
||||
|
||||
pub(crate) struct Data<T>(pub(crate) T);
|
||||
|
||||
impl<T: Clone + 'static> DataFactory for Data<T> {
|
||||
fn set(&self, ext: &mut Extensions) {
|
||||
ext.insert(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::from_utf8;
|
||||
|
@ -1,10 +1,12 @@
|
||||
//! Basic http responses
|
||||
//! Status code based HTTP response builders.
|
||||
|
||||
#![allow(non_upper_case_globals)]
|
||||
|
||||
use http::StatusCode;
|
||||
|
||||
use crate::response::{Response, ResponseBuilder};
|
||||
|
||||
macro_rules! STATIC_RESP {
|
||||
macro_rules! static_resp {
|
||||
($name:ident, $status:expr) => {
|
||||
#[allow(non_snake_case, missing_docs)]
|
||||
pub fn $name() -> ResponseBuilder {
|
||||
@ -14,63 +16,67 @@ macro_rules! STATIC_RESP {
|
||||
}
|
||||
|
||||
impl Response {
|
||||
STATIC_RESP!(Ok, StatusCode::OK);
|
||||
STATIC_RESP!(Created, StatusCode::CREATED);
|
||||
STATIC_RESP!(Accepted, StatusCode::ACCEPTED);
|
||||
STATIC_RESP!(
|
||||
static_resp!(Continue, StatusCode::CONTINUE);
|
||||
static_resp!(SwitchingProtocols, StatusCode::SWITCHING_PROTOCOLS);
|
||||
static_resp!(Processing, StatusCode::PROCESSING);
|
||||
|
||||
static_resp!(Ok, StatusCode::OK);
|
||||
static_resp!(Created, StatusCode::CREATED);
|
||||
static_resp!(Accepted, StatusCode::ACCEPTED);
|
||||
static_resp!(
|
||||
NonAuthoritativeInformation,
|
||||
StatusCode::NON_AUTHORITATIVE_INFORMATION
|
||||
);
|
||||
|
||||
STATIC_RESP!(NoContent, StatusCode::NO_CONTENT);
|
||||
STATIC_RESP!(ResetContent, StatusCode::RESET_CONTENT);
|
||||
STATIC_RESP!(PartialContent, StatusCode::PARTIAL_CONTENT);
|
||||
STATIC_RESP!(MultiStatus, StatusCode::MULTI_STATUS);
|
||||
STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED);
|
||||
static_resp!(NoContent, StatusCode::NO_CONTENT);
|
||||
static_resp!(ResetContent, StatusCode::RESET_CONTENT);
|
||||
static_resp!(PartialContent, StatusCode::PARTIAL_CONTENT);
|
||||
static_resp!(MultiStatus, StatusCode::MULTI_STATUS);
|
||||
static_resp!(AlreadyReported, StatusCode::ALREADY_REPORTED);
|
||||
|
||||
STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES);
|
||||
STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY);
|
||||
STATIC_RESP!(Found, StatusCode::FOUND);
|
||||
STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER);
|
||||
STATIC_RESP!(NotModified, StatusCode::NOT_MODIFIED);
|
||||
STATIC_RESP!(UseProxy, StatusCode::USE_PROXY);
|
||||
STATIC_RESP!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT);
|
||||
STATIC_RESP!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT);
|
||||
static_resp!(MultipleChoices, StatusCode::MULTIPLE_CHOICES);
|
||||
static_resp!(MovedPermanently, StatusCode::MOVED_PERMANENTLY);
|
||||
static_resp!(Found, StatusCode::FOUND);
|
||||
static_resp!(SeeOther, StatusCode::SEE_OTHER);
|
||||
static_resp!(NotModified, StatusCode::NOT_MODIFIED);
|
||||
static_resp!(UseProxy, StatusCode::USE_PROXY);
|
||||
static_resp!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT);
|
||||
static_resp!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT);
|
||||
|
||||
STATIC_RESP!(BadRequest, StatusCode::BAD_REQUEST);
|
||||
STATIC_RESP!(NotFound, StatusCode::NOT_FOUND);
|
||||
STATIC_RESP!(Unauthorized, StatusCode::UNAUTHORIZED);
|
||||
STATIC_RESP!(PaymentRequired, StatusCode::PAYMENT_REQUIRED);
|
||||
STATIC_RESP!(Forbidden, StatusCode::FORBIDDEN);
|
||||
STATIC_RESP!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED);
|
||||
STATIC_RESP!(NotAcceptable, StatusCode::NOT_ACCEPTABLE);
|
||||
STATIC_RESP!(
|
||||
static_resp!(BadRequest, StatusCode::BAD_REQUEST);
|
||||
static_resp!(NotFound, StatusCode::NOT_FOUND);
|
||||
static_resp!(Unauthorized, StatusCode::UNAUTHORIZED);
|
||||
static_resp!(PaymentRequired, StatusCode::PAYMENT_REQUIRED);
|
||||
static_resp!(Forbidden, StatusCode::FORBIDDEN);
|
||||
static_resp!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED);
|
||||
static_resp!(NotAcceptable, StatusCode::NOT_ACCEPTABLE);
|
||||
static_resp!(
|
||||
ProxyAuthenticationRequired,
|
||||
StatusCode::PROXY_AUTHENTICATION_REQUIRED
|
||||
);
|
||||
STATIC_RESP!(RequestTimeout, StatusCode::REQUEST_TIMEOUT);
|
||||
STATIC_RESP!(Conflict, StatusCode::CONFLICT);
|
||||
STATIC_RESP!(Gone, StatusCode::GONE);
|
||||
STATIC_RESP!(LengthRequired, StatusCode::LENGTH_REQUIRED);
|
||||
STATIC_RESP!(PreconditionFailed, StatusCode::PRECONDITION_FAILED);
|
||||
STATIC_RESP!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED);
|
||||
STATIC_RESP!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE);
|
||||
STATIC_RESP!(UriTooLong, StatusCode::URI_TOO_LONG);
|
||||
STATIC_RESP!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
STATIC_RESP!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE);
|
||||
STATIC_RESP!(ExpectationFailed, StatusCode::EXPECTATION_FAILED);
|
||||
STATIC_RESP!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY);
|
||||
STATIC_RESP!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS);
|
||||
static_resp!(RequestTimeout, StatusCode::REQUEST_TIMEOUT);
|
||||
static_resp!(Conflict, StatusCode::CONFLICT);
|
||||
static_resp!(Gone, StatusCode::GONE);
|
||||
static_resp!(LengthRequired, StatusCode::LENGTH_REQUIRED);
|
||||
static_resp!(PreconditionFailed, StatusCode::PRECONDITION_FAILED);
|
||||
static_resp!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED);
|
||||
static_resp!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE);
|
||||
static_resp!(UriTooLong, StatusCode::URI_TOO_LONG);
|
||||
static_resp!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
static_resp!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE);
|
||||
static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED);
|
||||
static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY);
|
||||
static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS);
|
||||
|
||||
STATIC_RESP!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
STATIC_RESP!(NotImplemented, StatusCode::NOT_IMPLEMENTED);
|
||||
STATIC_RESP!(BadGateway, StatusCode::BAD_GATEWAY);
|
||||
STATIC_RESP!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE);
|
||||
STATIC_RESP!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT);
|
||||
STATIC_RESP!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED);
|
||||
STATIC_RESP!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES);
|
||||
STATIC_RESP!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE);
|
||||
STATIC_RESP!(LoopDetected, StatusCode::LOOP_DETECTED);
|
||||
static_resp!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
static_resp!(NotImplemented, StatusCode::NOT_IMPLEMENTED);
|
||||
static_resp!(BadGateway, StatusCode::BAD_GATEWAY);
|
||||
static_resp!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE);
|
||||
static_resp!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT);
|
||||
static_resp!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED);
|
||||
static_resp!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES);
|
||||
static_resp!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE);
|
||||
static_resp!(LoopDetected, StatusCode::LOOP_DETECTED);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -167,7 +167,6 @@ where
|
||||
mod tests {
|
||||
use bytes::Bytes;
|
||||
use encoding_rs::ISO_8859_2;
|
||||
use mime;
|
||||
|
||||
use super::*;
|
||||
use crate::test::TestRequest;
|
||||
|
@ -1,11 +1,14 @@
|
||||
//! 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
|
||||
)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
@ -16,7 +19,6 @@ mod macros;
|
||||
pub mod body;
|
||||
mod builder;
|
||||
pub mod client;
|
||||
mod cloneable;
|
||||
mod config;
|
||||
#[cfg(feature = "compress")]
|
||||
pub mod encoding;
|
||||
@ -70,9 +72,49 @@ pub mod http {
|
||||
pub use crate::message::ConnectionType;
|
||||
}
|
||||
|
||||
/// Http protocol
|
||||
/// A major HTTP protocol version.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum Protocol {
|
||||
Http1,
|
||||
Http2,
|
||||
Http3,
|
||||
}
|
||||
|
||||
type ConnectCallback<IO> = dyn Fn(&IO, &mut Extensions);
|
||||
|
||||
/// Container for data that extract with ConnectCallback.
|
||||
///
|
||||
/// # Implementation Details
|
||||
/// Uses Option to reduce necessary allocations when merging with request extensions.
|
||||
pub(crate) struct OnConnectData(Option<Extensions>);
|
||||
|
||||
impl Default for OnConnectData {
|
||||
fn default() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl OnConnectData {
|
||||
/// Construct by calling the on-connect callback with the underlying transport I/O.
|
||||
pub(crate) fn from_io<T>(
|
||||
io: &T,
|
||||
on_connect_ext: Option<&ConnectCallback<T>>,
|
||||
) -> Self {
|
||||
let ext = on_connect_ext.map(|handler| {
|
||||
let mut extensions = Extensions::new();
|
||||
handler(io, &mut extensions);
|
||||
extensions
|
||||
});
|
||||
|
||||
Self(ext)
|
||||
}
|
||||
|
||||
/// Merge self into given request's extensions.
|
||||
#[inline]
|
||||
pub(crate) fn merge_into(&mut self, req: &mut Request) {
|
||||
if let Some(ref mut ext) = self.0 {
|
||||
req.head.extensions.get_mut().drain_from(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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::<()>();
|
||||
|
@ -67,7 +67,7 @@ impl Head for RequestHead {
|
||||
fn clear(&mut self) {
|
||||
self.flags = Flags::empty();
|
||||
self.headers.clear();
|
||||
self.extensions.borrow_mut().clear();
|
||||
self.extensions.get_mut().clear();
|
||||
}
|
||||
|
||||
fn pool() -> &'static MessagePool<Self> {
|
||||
@ -440,9 +440,11 @@ impl<T: Head> MessagePool<T> {
|
||||
#[inline]
|
||||
fn get_message(&'static self) -> Message<T> {
|
||||
if let Some(mut msg) = self.0.borrow_mut().pop() {
|
||||
if let Some(r) = Rc::get_mut(&mut msg) {
|
||||
r.clear();
|
||||
}
|
||||
// Message is put in pool only when it's the last copy.
|
||||
// which means it's guaranteed to be unique when popped out.
|
||||
Rc::get_mut(&mut msg)
|
||||
.expect("Multiple copies exist")
|
||||
.clear();
|
||||
Message { head: msg }
|
||||
} else {
|
||||
Message {
|
||||
|
@ -23,6 +23,10 @@ impl<P> HttpMessage for Request<P> {
|
||||
&self.head().headers
|
||||
}
|
||||
|
||||
fn take_payload(&mut self) -> Payload<P> {
|
||||
std::mem::replace(&mut self.payload, Payload::None)
|
||||
}
|
||||
|
||||
/// Request extensions
|
||||
#[inline]
|
||||
fn extensions(&self) -> Ref<'_, Extensions> {
|
||||
@ -34,10 +38,6 @@ impl<P> HttpMessage for Request<P> {
|
||||
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
|
||||
self.head.extensions_mut()
|
||||
}
|
||||
|
||||
fn take_payload(&mut self) -> Payload<P> {
|
||||
std::mem::replace(&mut self.payload, Payload::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message<RequestHead>> for Request<PayloadStream> {
|
||||
|
@ -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,44 +1,42 @@
|
||||
use std::cell::RefCell;
|
||||
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;
|
||||
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory};
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Future};
|
||||
use futures_util::future::ok;
|
||||
use h2::server::{self, Handshake};
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::body::MessageBody;
|
||||
use crate::builder::HttpServiceBuilder;
|
||||
use crate::cloneable::CloneableService;
|
||||
use crate::config::{KeepAlive, ServiceConfig};
|
||||
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, OnConnectData, Protocol};
|
||||
|
||||
/// `ServiceFactory` HTTP1.1/HTTP2 transport implementation
|
||||
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler<T>> {
|
||||
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
|
||||
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
|
||||
srv: S,
|
||||
cfg: ServiceConfig,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
_t: PhantomData<(T, B)>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<T, S, B> HttpService<T, S, B>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create builder for `HttpService` instance.
|
||||
@ -49,15 +47,15 @@ where
|
||||
|
||||
impl<T, S, B> HttpService<T, S, B>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
/// Create new `HttpService` instance.
|
||||
pub fn new<F: IntoServiceFactory<S>>(service: F) -> Self {
|
||||
pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self {
|
||||
let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0, false, None);
|
||||
|
||||
HttpService {
|
||||
@ -65,13 +63,13 @@ where
|
||||
srv: service.into_factory(),
|
||||
expect: h1::ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect: None,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: None,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new `HttpService` instance with config.
|
||||
pub(crate) fn with_config<F: IntoServiceFactory<S>>(
|
||||
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
|
||||
cfg: ServiceConfig,
|
||||
service: F,
|
||||
) -> Self {
|
||||
@ -80,19 +78,19 @@ where
|
||||
srv: service.into_factory(),
|
||||
expect: h1::ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect: None,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: None,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> HttpService<T, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody,
|
||||
{
|
||||
/// Provide service for `EXPECT: 100-Continue` support.
|
||||
@ -102,18 +100,18 @@ where
|
||||
/// request will be forwarded to main service.
|
||||
pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U>
|
||||
where
|
||||
X1: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X1::Error: Into<Error>,
|
||||
X1::InitError: fmt::Debug,
|
||||
<X1::Service as Service>::Future: 'static,
|
||||
<X1::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
HttpService {
|
||||
expect,
|
||||
cfg: self.cfg,
|
||||
srv: self.srv,
|
||||
upgrade: self.upgrade,
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,69 +121,62 @@ where
|
||||
/// and this service get called with original request and framed object.
|
||||
pub fn upgrade<U1>(self, upgrade: Option<U1>) -> HttpService<T, S, B, X, U1>
|
||||
where
|
||||
U1: ServiceFactory<
|
||||
Config = (),
|
||||
Request = (Request, Framed<T, h1::Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||
U1::Error: fmt::Display,
|
||||
U1::InitError: fmt::Debug,
|
||||
<U1::Service as Service>::Future: 'static,
|
||||
<U1::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
|
||||
{
|
||||
HttpService {
|
||||
upgrade,
|
||||
cfg: self.cfg,
|
||||
srv: self.srv,
|
||||
expect: self.expect,
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set on connect callback.
|
||||
pub(crate) fn on_connect(
|
||||
mut self,
|
||||
f: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
) -> Self {
|
||||
self.on_connect = f;
|
||||
/// 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>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
<X::Service as Service>::Future: 'static,
|
||||
<X::Service as Service<Request>>::Future: 'static,
|
||||
U: ServiceFactory<
|
||||
(Request, Framed<TcpStream, h1::Codec>),
|
||||
Config = (),
|
||||
Request = (Request, Framed<TcpStream, h1::Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
U::InitError: fmt::Debug,
|
||||
<U::Service as Service>::Future: 'static,
|
||||
<U::Service as Service<(Request, Framed<TcpStream, h1::Codec>)>>::Future: 'static,
|
||||
{
|
||||
/// Create simple tcp stream service
|
||||
pub fn tcp(
|
||||
self,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = DispatchError,
|
||||
InitError = (),
|
||||
> {
|
||||
pipeline_factory(|io: TcpStream| {
|
||||
pipeline_factory(|io: TcpStream| async {
|
||||
let peer_addr = io.peer_addr().ok();
|
||||
ok((io, Protocol::Http1, peer_addr))
|
||||
Ok((io, Protocol::Http1, peer_addr))
|
||||
})
|
||||
.and_then(self)
|
||||
}
|
||||
@ -194,47 +185,48 @@ where
|
||||
#[cfg(feature = "openssl")]
|
||||
mod openssl {
|
||||
use super::*;
|
||||
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
|
||||
use actix_tls::{openssl::HandshakeError, SslError};
|
||||
use actix_service::ServiceFactoryExt;
|
||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
|
||||
impl<S, B, X, U> HttpService<SslStream<TcpStream>, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
<X::Service as Service>::Future: 'static,
|
||||
<X::Service as Service<Request>>::Future: 'static,
|
||||
U: ServiceFactory<
|
||||
(Request, Framed<SslStream<TcpStream>, h1::Codec>),
|
||||
Config = (),
|
||||
Request = (Request, Framed<SslStream<TcpStream>, h1::Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
U::InitError: fmt::Debug,
|
||||
<U::Service as Service>::Future: 'static,
|
||||
<U::Service as Service<(Request, Framed<SslStream<TcpStream>, h1::Codec>)>>::Future: 'static,
|
||||
{
|
||||
/// Create openssl based service
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
Config = (),
|
||||
Request = TcpStream,
|
||||
Response = (),
|
||||
Error = SslError<HandshakeError<TcpStream>, DispatchError>,
|
||||
Error = TlsError<SslError, DispatchError>,
|
||||
InitError = (),
|
||||
> {
|
||||
pipeline_factory(
|
||||
Acceptor::new(acceptor)
|
||||
.map_err(SslError::Ssl)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(|io: SslStream<TcpStream>| {
|
||||
.and_then(|io: SslStream<TcpStream>| async {
|
||||
let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() {
|
||||
if protos.windows(2).any(|window| window == b"h2") {
|
||||
Protocol::Http2
|
||||
@ -245,50 +237,53 @@ mod openssl {
|
||||
Protocol::Http1
|
||||
};
|
||||
let peer_addr = io.get_ref().peer_addr().ok();
|
||||
ok((io, proto, peer_addr))
|
||||
Ok((io, proto, peer_addr))
|
||||
})
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
mod rustls {
|
||||
use super::*;
|
||||
use actix_tls::rustls::{Acceptor, ServerConfig, Session, TlsStream};
|
||||
use actix_tls::SslError;
|
||||
use std::io;
|
||||
|
||||
use actix_tls::accept::rustls::{Acceptor, ServerConfig, Session, TlsStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
|
||||
use super::*;
|
||||
use actix_service::ServiceFactoryExt;
|
||||
|
||||
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
<X::Service as Service>::Future: 'static,
|
||||
<X::Service as Service<Request>>::Future: 'static,
|
||||
U: ServiceFactory<
|
||||
(Request, Framed<TlsStream<TcpStream>, h1::Codec>),
|
||||
Config = (),
|
||||
Request = (Request, Framed<TlsStream<TcpStream>, h1::Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
U::InitError: fmt::Debug,
|
||||
<U::Service as Service>::Future: 'static,
|
||||
<U::Service as Service<(Request, Framed<TlsStream<TcpStream>, h1::Codec>)>>::Future: 'static,
|
||||
{
|
||||
/// Create openssl based service
|
||||
pub fn rustls(
|
||||
self,
|
||||
mut config: ServerConfig,
|
||||
) -> impl ServiceFactory<
|
||||
TcpStream,
|
||||
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,10 +291,10 @@ mod rustls {
|
||||
|
||||
pipeline_factory(
|
||||
Acceptor::new(config)
|
||||
.map_err(SslError::Ssl)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(|io: TlsStream<TcpStream>| {
|
||||
.and_then(|io: TlsStream<TcpStream>| async {
|
||||
let proto = if let Some(protos) = io.get_ref().1.get_alpn_protocol() {
|
||||
if protos.windows(2).any(|window| window == b"h2") {
|
||||
Protocol::Http2
|
||||
@ -310,41 +305,37 @@ mod rustls {
|
||||
Protocol::Http1
|
||||
};
|
||||
let peer_addr = io.get_ref().0.peer_addr().ok();
|
||||
ok((io, proto, peer_addr))
|
||||
Ok((io, proto, peer_addr))
|
||||
})
|
||||
.and_then(self.map_err(SslError::Service))
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> ServiceFactory for HttpService<T, S, B, X, U>
|
||||
impl<T, S, B, X, U> ServiceFactory<(T, Protocol, Option<net::SocketAddr>)>
|
||||
for HttpService<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
<X::Service as Service>::Future: 'static,
|
||||
U: ServiceFactory<
|
||||
Config = (),
|
||||
Request = (Request, Framed<T, h1::Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
<X::Service as Service<Request>>::Future: 'static,
|
||||
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
U::InitError: fmt::Debug,
|
||||
<U::Service as Service>::Future: 'static,
|
||||
<U::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
|
||||
{
|
||||
type Config = ();
|
||||
type Request = (T, Protocol, Option<net::SocketAddr>);
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
type InitError = ();
|
||||
type Config = ();
|
||||
type Service = HttpServiceHandler<T, S::Service, B, X::Service, U::Service>;
|
||||
type InitError = ();
|
||||
type Future = HttpServiceResponse<T, S, B, X, U>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
@ -354,22 +345,21 @@ where
|
||||
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())),
|
||||
expect: None,
|
||||
upgrade: None,
|
||||
on_connect: self.on_connect.clone(),
|
||||
on_connect_ext: self.on_connect_ext.clone(),
|
||||
cfg: self.cfg.clone(),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[pin_project]
|
||||
pub struct HttpServiceResponse<
|
||||
T,
|
||||
S: ServiceFactory,
|
||||
B,
|
||||
X: ServiceFactory,
|
||||
U: ServiceFactory,
|
||||
> {
|
||||
pub struct HttpServiceResponse<T, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Request>,
|
||||
X: ServiceFactory<Request>,
|
||||
U: ServiceFactory<(Request, Framed<T, h1::Codec>)>,
|
||||
{
|
||||
#[pin]
|
||||
fut: S::Future,
|
||||
#[pin]
|
||||
@ -378,28 +368,28 @@ 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_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
cfg: ServiceConfig,
|
||||
_t: PhantomData<(T, B)>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> Future for HttpServiceResponse<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: ServiceFactory<Request = Request>,
|
||||
S: ServiceFactory<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
X: ServiceFactory<Request = Request, Response = Request>,
|
||||
X: ServiceFactory<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
<X::Service as Service>::Future: 'static,
|
||||
U: ServiceFactory<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
<X::Service as Service<Request>>::Future: 'static,
|
||||
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
U::InitError: fmt::Debug,
|
||||
<U::Service as Service>::Future: 'static,
|
||||
<U::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
|
||||
{
|
||||
type Output =
|
||||
Result<HttpServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
|
||||
@ -422,13 +412,14 @@ where
|
||||
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
|
||||
this = self.as_mut().project();
|
||||
*this.upgrade = Some(upgrade);
|
||||
this.fut_ex.set(None);
|
||||
this.fut_upg.set(None);
|
||||
}
|
||||
|
||||
let result = ready!(this
|
||||
.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(
|
||||
@ -436,72 +427,91 @@ where
|
||||
service,
|
||||
this.expect.take().unwrap(),
|
||||
this.upgrade.take(),
|
||||
this.on_connect.clone(),
|
||||
this.on_connect_ext.clone(),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// `Service` implementation for http transport
|
||||
pub struct HttpServiceHandler<T, S: Service, B, X: Service, U: Service> {
|
||||
srv: CloneableService<S>,
|
||||
expect: CloneableService<X>,
|
||||
upgrade: Option<CloneableService<U>>,
|
||||
pub struct HttpServiceHandler<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
X: Service<Request>,
|
||||
U: Service<(Request, Framed<T, h1::Codec>)>,
|
||||
{
|
||||
flow: Rc<RefCell<HttpFlow<S, X, U>>>,
|
||||
cfg: ServiceConfig,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
_t: PhantomData<(T, B, X)>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
/// A collection of services that describe an HTTP request flow.
|
||||
pub(super) struct HttpFlow<S, X, U> {
|
||||
pub(super) service: S,
|
||||
pub(super) expect: X,
|
||||
pub(super) upgrade: Option<U>,
|
||||
}
|
||||
|
||||
impl<S, X, U> HttpFlow<S, X, U> {
|
||||
pub(super) fn new(service: S, expect: X, upgrade: Option<U>) -> Rc<RefCell<Self>> {
|
||||
Rc::new(RefCell::new(Self {
|
||||
service,
|
||||
expect,
|
||||
upgrade,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
X: Service<Request = Request, Response = Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
fn new(
|
||||
cfg: ServiceConfig,
|
||||
srv: S,
|
||||
service: S,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
) -> HttpServiceHandler<T, S, B, X, U> {
|
||||
HttpServiceHandler {
|
||||
cfg,
|
||||
on_connect,
|
||||
srv: CloneableService::new(srv),
|
||||
expect: CloneableService::new(expect),
|
||||
upgrade: upgrade.map(CloneableService::new),
|
||||
_t: PhantomData,
|
||||
on_connect_ext,
|
||||
flow: HttpFlow::new(service, expect, upgrade),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> Service for HttpServiceHandler<T, S, B, X, U>
|
||||
impl<T, S, B, X, U> Service<(T, Protocol, Option<net::SocketAddr>)>
|
||||
for HttpServiceHandler<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
X: Service<Request = Request, Response = Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display + Into<Error>,
|
||||
{
|
||||
type Request = (T, Protocol, Option<net::SocketAddr>);
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
type Future = HttpServiceHandlerResponse<T, S, B, X, U>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
let ready = self
|
||||
let mut flow = self.flow.borrow_mut();
|
||||
let ready = flow
|
||||
.expect
|
||||
.poll_ready(cx)
|
||||
.map_err(|e| {
|
||||
@ -511,8 +521,8 @@ where
|
||||
})?
|
||||
.is_ready();
|
||||
|
||||
let ready = self
|
||||
.srv
|
||||
let ready = flow
|
||||
.service
|
||||
.poll_ready(cx)
|
||||
.map_err(|e| {
|
||||
let e = e.into();
|
||||
@ -522,7 +532,7 @@ where
|
||||
.is_ready()
|
||||
&& ready;
|
||||
|
||||
let ready = if let Some(ref mut upg) = self.upgrade {
|
||||
let ready = if let Some(ref mut upg) = flow.upgrade {
|
||||
upg.poll_ready(cx)
|
||||
.map_err(|e| {
|
||||
let e = e.into();
|
||||
@ -542,34 +552,35 @@ 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
|
||||
};
|
||||
fn call(
|
||||
&mut self,
|
||||
(io, proto, peer_addr): (T, Protocol, Option<net::SocketAddr>),
|
||||
) -> Self::Future {
|
||||
let on_connect_data =
|
||||
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||
|
||||
match proto {
|
||||
Protocol::Http2 => HttpServiceHandlerResponse {
|
||||
state: State::H2Handshake(Some((
|
||||
server::handshake(io),
|
||||
self.cfg.clone(),
|
||||
self.srv.clone(),
|
||||
on_connect,
|
||||
self.flow.clone(),
|
||||
on_connect_data,
|
||||
peer_addr,
|
||||
))),
|
||||
},
|
||||
|
||||
Protocol::Http1 => HttpServiceHandlerResponse {
|
||||
state: State::H1(h1::Dispatcher::new(
|
||||
io,
|
||||
self.cfg.clone(),
|
||||
self.srv.clone(),
|
||||
self.expect.clone(),
|
||||
self.upgrade.clone(),
|
||||
on_connect,
|
||||
self.flow.clone(),
|
||||
on_connect_data,
|
||||
peer_addr,
|
||||
)),
|
||||
},
|
||||
|
||||
proto => unimplemented!("Unsupported HTTP version: {:?}.", proto),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -577,24 +588,24 @@ where
|
||||
#[pin_project(project = StateProj)]
|
||||
enum State<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Error>,
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
B: MessageBody,
|
||||
X: Service<Request = Request, Response = Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
H1(#[pin] h1::Dispatcher<T, S, B, X, U>),
|
||||
H2(#[pin] Dispatcher<T, S, B>),
|
||||
H2(#[pin] Dispatcher<T, S, B, X, U>),
|
||||
H2Handshake(
|
||||
Option<(
|
||||
Handshake<T, Bytes>,
|
||||
ServiceConfig,
|
||||
CloneableService<S>,
|
||||
Option<Box<dyn DataFactory>>,
|
||||
Rc<RefCell<HttpFlow<S, X, U>>>,
|
||||
OnConnectData,
|
||||
Option<net::SocketAddr>,
|
||||
)>,
|
||||
),
|
||||
@ -604,14 +615,14 @@ where
|
||||
pub struct HttpServiceHandlerResponse<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
X: Service<Request = Request, Response = Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
#[pin]
|
||||
@ -621,60 +632,42 @@ where
|
||||
impl<T, S, B, X, U> Future for HttpServiceHandlerResponse<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody,
|
||||
X: Service<Request = Request, Response = Request>,
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
type Output = Result<(), DispatchError>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
self.project().state.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> State<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request = Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
X: Service<Request = Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
fn poll(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), DispatchError>> {
|
||||
match self.as_mut().project() {
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.as_mut().project().state.project() {
|
||||
StateProj::H1(disp) => disp.poll(cx),
|
||||
StateProj::H2(disp) => disp.poll(cx),
|
||||
StateProj::H2Handshake(ref mut data) => {
|
||||
let conn = if let Some(ref mut item) = data {
|
||||
match Pin::new(&mut item.0).poll(cx) {
|
||||
Poll::Ready(Ok(conn)) => conn,
|
||||
Poll::Ready(Err(err)) => {
|
||||
trace!("H2 handshake error: {}", err);
|
||||
return Poll::Ready(Err(err.into()));
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
StateProj::H2Handshake(data) => {
|
||||
match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) {
|
||||
Ok(conn) => {
|
||||
let (_, cfg, srv, on_connect_data, peer_addr) =
|
||||
data.take().unwrap();
|
||||
self.as_mut().project().state.set(State::H2(Dispatcher::new(
|
||||
srv,
|
||||
conn,
|
||||
on_connect_data,
|
||||
cfg,
|
||||
None,
|
||||
peer_addr,
|
||||
)));
|
||||
self.poll(cx)
|
||||
}
|
||||
} else {
|
||||
panic!()
|
||||
};
|
||||
let (_, cfg, srv, on_connect, peer_addr) = data.take().unwrap();
|
||||
self.set(State::H2(Dispatcher::new(
|
||||
srv, conn, on_connect, cfg, None, peer_addr,
|
||||
)));
|
||||
self.poll(cx)
|
||||
Err(err) => {
|
||||
trace!("H2 handshake error: {}", err);
|
||||
Poll::Ready(Err(err.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
//! Test Various helpers for Actix applications to use during testing.
|
||||
use std::convert::TryFrom;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::task::{Context, Poll};
|
||||
//! Various testing helpers for use in internal and app tests.
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use std::{
|
||||
cell::{Ref, RefCell},
|
||||
convert::TryFrom,
|
||||
io::{self, Read, Write},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use http::header::{self, HeaderName, HeaderValue};
|
||||
use http::{Error as HttpError, Method, Uri, Version};
|
||||
@ -183,7 +188,7 @@ fn parts(parts: &mut Option<Inner>) -> &mut Inner {
|
||||
parts.as_mut().expect("cannot reuse test request builder")
|
||||
}
|
||||
|
||||
/// Async io buffer
|
||||
/// Async I/O test buffer.
|
||||
pub struct TestBuffer {
|
||||
pub read_buf: BytesMut,
|
||||
pub write_buf: BytesMut,
|
||||
@ -191,24 +196,24 @@ pub struct TestBuffer {
|
||||
}
|
||||
|
||||
impl TestBuffer {
|
||||
/// Create new TestBuffer instance
|
||||
pub fn new<T>(data: T) -> TestBuffer
|
||||
/// Create new `TestBuffer` instance with initial read buffer.
|
||||
pub fn new<T>(data: T) -> Self
|
||||
where
|
||||
BytesMut: From<T>,
|
||||
T: Into<BytesMut>,
|
||||
{
|
||||
TestBuffer {
|
||||
read_buf: BytesMut::from(data),
|
||||
Self {
|
||||
read_buf: data.into(),
|
||||
write_buf: BytesMut::new(),
|
||||
err: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new empty TestBuffer instance
|
||||
pub fn empty() -> TestBuffer {
|
||||
TestBuffer::new("")
|
||||
/// Create new empty `TestBuffer` instance.
|
||||
pub fn empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
/// Add extra data to read buffer.
|
||||
/// Add data to read buffer.
|
||||
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
|
||||
self.read_buf.extend_from_slice(data.as_ref())
|
||||
}
|
||||
@ -236,6 +241,7 @@ impl io::Write for TestBuffer {
|
||||
self.write_buf.extend(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
@ -245,9 +251,11 @@ impl AsyncRead for TestBuffer {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Poll::Ready(self.get_mut().read(buf))
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let dst = buf.initialize_unfilled();
|
||||
let res = self.get_mut().read(dst).map(|n| buf.advance(n));
|
||||
Poll::Ready(res)
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,3 +276,117 @@ impl AsyncWrite for TestBuffer {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Async I/O test buffer with ability to incrementally add to the read buffer.
|
||||
#[derive(Clone)]
|
||||
pub struct TestSeqBuffer(Rc<RefCell<TestSeqInner>>);
|
||||
|
||||
impl TestSeqBuffer {
|
||||
/// Create new `TestBuffer` instance with initial read buffer.
|
||||
pub fn new<T>(data: T) -> Self
|
||||
where
|
||||
T: Into<BytesMut>,
|
||||
{
|
||||
Self(Rc::new(RefCell::new(TestSeqInner {
|
||||
read_buf: data.into(),
|
||||
write_buf: BytesMut::new(),
|
||||
err: None,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Create new empty `TestBuffer` instance.
|
||||
pub fn empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
pub fn read_buf(&self) -> Ref<'_, BytesMut> {
|
||||
Ref::map(self.0.borrow(), |inner| &inner.read_buf)
|
||||
}
|
||||
|
||||
pub fn write_buf(&self) -> Ref<'_, BytesMut> {
|
||||
Ref::map(self.0.borrow(), |inner| &inner.write_buf)
|
||||
}
|
||||
|
||||
pub fn err(&self) -> Ref<'_, Option<io::Error>> {
|
||||
Ref::map(self.0.borrow(), |inner| &inner.err)
|
||||
}
|
||||
|
||||
/// Add data to read buffer.
|
||||
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.read_buf
|
||||
.extend_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestSeqInner {
|
||||
read_buf: BytesMut,
|
||||
write_buf: BytesMut,
|
||||
err: Option<io::Error>,
|
||||
}
|
||||
|
||||
impl io::Read for TestSeqBuffer {
|
||||
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
|
||||
if self.0.borrow().read_buf.is_empty() {
|
||||
if self.0.borrow().err.is_some() {
|
||||
Err(self.0.borrow_mut().err.take().unwrap())
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
|
||||
}
|
||||
} else {
|
||||
let size = std::cmp::min(self.0.borrow().read_buf.len(), dst.len());
|
||||
let b = self.0.borrow_mut().read_buf.split_to(size);
|
||||
dst[..size].copy_from_slice(&b);
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Write for TestSeqBuffer {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.0.borrow_mut().write_buf.extend(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for TestSeqBuffer {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let dst = buf.initialize_unfilled();
|
||||
let r = self.get_mut().read(dst);
|
||||
match r {
|
||||
Ok(n) => {
|
||||
buf.advance(n);
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::WouldBlock => Poll::Pending,
|
||||
Err(err) => Poll::Ready(Err(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for TestSeqBuffer {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Poll::Ready(self.get_mut().write(buf))
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,60 @@
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
use bitflags::bitflags;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
|
||||
use super::frame::Parser;
|
||||
use super::proto::{CloseReason, OpCode};
|
||||
use super::ProtocolError;
|
||||
|
||||
/// `WebSocket` Message
|
||||
/// A WebSocket message.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Message {
|
||||
/// Text message
|
||||
Text(String),
|
||||
/// Binary message
|
||||
/// Text message.
|
||||
Text(ByteString),
|
||||
|
||||
/// Binary message.
|
||||
Binary(Bytes),
|
||||
/// Continuation
|
||||
|
||||
/// Continuation.
|
||||
Continuation(Item),
|
||||
/// Ping message
|
||||
|
||||
/// Ping message.
|
||||
Ping(Bytes),
|
||||
/// Pong message
|
||||
|
||||
/// Pong message.
|
||||
Pong(Bytes),
|
||||
/// Close message with optional reason
|
||||
|
||||
/// Close message with optional reason.
|
||||
Close(Option<CloseReason>),
|
||||
/// No-op. Useful for actix-net services
|
||||
|
||||
/// No-op. Useful for low-level services.
|
||||
Nop,
|
||||
}
|
||||
|
||||
/// `WebSocket` frame
|
||||
/// A WebSocket frame.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Frame {
|
||||
/// Text frame, codec does not verify utf8 encoding
|
||||
/// Text frame. Note that the codec does not validate UTF-8 encoding.
|
||||
Text(Bytes),
|
||||
/// Binary frame
|
||||
|
||||
/// Binary frame.
|
||||
Binary(Bytes),
|
||||
/// Continuation
|
||||
|
||||
/// Continuation.
|
||||
Continuation(Item),
|
||||
/// Ping message
|
||||
|
||||
/// Ping message.
|
||||
Ping(Bytes),
|
||||
/// Pong message
|
||||
|
||||
/// Pong message.
|
||||
Pong(Bytes),
|
||||
/// Close message with optional reason
|
||||
|
||||
/// Close message with optional reason.
|
||||
Close(Option<CloseReason>),
|
||||
}
|
||||
|
||||
/// `WebSocket` continuation item
|
||||
/// A `WebSocket` continuation item.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Item {
|
||||
FirstText(Bytes),
|
||||
@ -51,13 +64,13 @@ pub enum Item {
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
/// WebSockets protocol codec
|
||||
/// WebSocket protocol codec.
|
||||
pub struct Codec {
|
||||
flags: Flags,
|
||||
max_size: usize,
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
const SERVER = 0b0000_0001;
|
||||
const CONTINUATION = 0b0000_0010;
|
||||
@ -66,7 +79,7 @@ bitflags::bitflags! {
|
||||
}
|
||||
|
||||
impl Codec {
|
||||
/// Create new websocket frames decoder
|
||||
/// Create new websocket frames decoder.
|
||||
pub fn new() -> Codec {
|
||||
Codec {
|
||||
max_size: 65_536,
|
||||
@ -74,9 +87,9 @@ impl Codec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set max frame size
|
||||
/// Set max frame size.
|
||||
///
|
||||
/// By default max size is set to 64kb
|
||||
/// By default max size is set to 64kb.
|
||||
pub fn max_size(mut self, size: usize) -> Self {
|
||||
self.max_size = size;
|
||||
self
|
||||
@ -91,8 +104,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> {
|
||||
@ -185,7 +197,7 @@ impl Encoder for Codec {
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::Nop => (),
|
||||
Message::Nop => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -4,34 +4,36 @@ 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,
|
||||
S: Service<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>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
S: Service<Request = Frame, Response = Message>,
|
||||
S: Service<Frame, Response = Message>,
|
||||
S::Future: 'static,
|
||||
S::Error: 'static,
|
||||
{
|
||||
pub fn new<F: IntoService<S>>(io: T, service: F) -> Self {
|
||||
pub fn new<F: IntoService<S, Frame>>(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 {
|
||||
pub fn with<F: IntoService<S, Frame>>(framed: Framed<T, Codec>, service: F) -> Self {
|
||||
Dispatcher {
|
||||
inner: framed::Dispatcher::new(framed, service),
|
||||
inner: InnerDispatcher::new(framed, service),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,13 +41,13 @@ where
|
||||
impl<S, T> Future for Dispatcher<S, T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
S: Service<Request = Frame, Response = Message>,
|
||||
S: Service<Frame, Response = Message>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ impl Parser {
|
||||
debug!("Received close frame with payload length exceeding 125. Morphing to protocol close frame.");
|
||||
return Ok(Some((true, OpCode::Close, None)));
|
||||
}
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// unmask
|
||||
@ -229,10 +229,7 @@ mod tests {
|
||||
fn is_none(
|
||||
frm: &Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError>,
|
||||
) -> bool {
|
||||
match *frm {
|
||||
Ok(None) => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(*frm, Ok(None))
|
||||
}
|
||||
|
||||
fn extract(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user