mirror of
https://github.com/fafhrd91/actix-web
synced 2025-08-19 20:35:36 +02:00
Compare commits
385 Commits
actors-v4.
...
on-connect
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aee8a1a58 | ||
|
|
bed72d9bb7 | ||
|
|
cca0593df1 | ||
|
|
c596f573a6 | ||
|
|
627c0dc22f | ||
|
|
2d053b7036 | ||
|
|
efa68ec453 | ||
|
|
01885f9954 | ||
|
|
a86c831b89 | ||
|
|
59be0c65c6 | ||
|
|
e1a2d9c606 | ||
|
|
d89c706cd6 | ||
|
|
4c9ca7196d | ||
|
|
fa7f3e6908 | ||
|
|
c7c02ef99d | ||
|
|
a2d5c5a058 | ||
|
|
deece8d519 | ||
|
|
2a72bdae09 | ||
|
|
075d871e63 | ||
|
|
c4b20df56a | ||
|
|
0df275c478 | ||
|
|
697238fadc | ||
|
|
e045418038 | ||
|
|
a978b417f3 | ||
|
|
fa82b698b7 | ||
|
|
fc4cdf81eb | ||
|
|
654dc64a09 | ||
|
|
cf54388534 | ||
|
|
39243095b5 | ||
|
|
89c6d62656 | ||
|
|
52bbbd1d73 | ||
|
|
3e6e9779dc | ||
|
|
9bdd334bb4 | ||
|
|
bcbbc115aa | ||
|
|
ab5eb7c1aa | ||
|
|
18b8ef0765 | ||
|
|
b806b4773c | ||
|
|
0062d99b6f | ||
|
|
99e6a9c26d | ||
|
|
5f5bd2184e | ||
|
|
88e074879d | ||
|
|
e7987e7429 | ||
|
|
a172f5968d | ||
|
|
a2a42ec152 | ||
|
|
dd347e0bd0 | ||
|
|
194a691537 | ||
|
|
56ee97f722 | ||
|
|
66620a1012 | ||
|
|
e33618ed6d | ||
|
|
1fe309bcc6 | ||
|
|
168a7284d3 | ||
|
|
68a3acb9c2 | ||
|
|
84c6d25fd3 | ||
|
|
0a135c7dc9 | ||
|
|
668a33c793 | ||
|
|
d8cbb879dd | ||
|
|
13cf5a9e44 | ||
|
|
4df1cd78b7 | ||
|
|
e8a0e16863 | ||
|
|
a2f59c02f7 | ||
|
|
2754608f3c | ||
|
|
c020cedb63 | ||
|
|
5e554dca35 | ||
|
|
6ec2d7b909 | ||
|
|
ec6d284a8e | ||
|
|
be9530eb72 | ||
|
|
855e260fdb | ||
|
|
d13854505f | ||
|
|
d40b6748bc | ||
|
|
c79b9a0df3 | ||
|
|
4af414064b | ||
|
|
9abe166d52 | ||
|
|
c09ec6af4c | ||
|
|
37f2bf5625 | ||
|
|
4f6f0b0137 | ||
|
|
591abc37c3 | ||
|
|
ad22cc4e7f | ||
|
|
efdf3ab1c3 | ||
|
|
6b3ea4fc61 | ||
|
|
99985fc4ec | ||
|
|
a6707fb7ee | ||
|
|
a3806cde19 | ||
|
|
efefa0d0ce | ||
|
|
450ff5fa1d | ||
|
|
8ae278cb68 | ||
|
|
46699e3429 | ||
|
|
ba88d3b4bf | ||
|
|
8dd30611fa | ||
|
|
1383c7d701 | ||
|
|
d8a0f46f26 | ||
|
|
53ec66caf4 | ||
|
|
93112644d3 | ||
|
|
ddc8c16cb3 | ||
|
|
373b3f91df | ||
|
|
7d01ece355 | ||
|
|
c50eef6166 | ||
|
|
dade818eba | ||
|
|
ae35e69382 | ||
|
|
5128b1bdfc | ||
|
|
168b2f227d | ||
|
|
4bb32fb19b | ||
|
|
f9da6e48e0 | ||
|
|
ff07816b65 | ||
|
|
5f412c67db | ||
|
|
a0c0bff944 | ||
|
|
384164cc14 | ||
|
|
e965d8298f | ||
|
|
f6e69919ed | ||
|
|
999c003aa8 | ||
|
|
2bc7102e37 | ||
|
|
20752fd82e | ||
|
|
e6290dfd09 | ||
|
|
9e685fc5fb | ||
|
|
cf63f5c755 | ||
|
|
694cfc94c9 | ||
|
|
6bb33ec5db | ||
|
|
3b2e2acb6c | ||
|
|
293c52c3ef | ||
|
|
5a14ffeef2 | ||
|
|
7ae132cb68 | ||
|
|
d8deed0475 | ||
|
|
2504c2ecb0 | ||
|
|
604be5495f | ||
|
|
262c6bc828 | ||
|
|
5eba95b731 | ||
|
|
09afd033fc | ||
|
|
539697292a | ||
|
|
5a480d1d78 | ||
|
|
9a26393375 | ||
|
|
2eacb735a4 | ||
|
|
767e4efe22 | ||
|
|
e559a197cc | ||
|
|
93aa86e30b | ||
|
|
2d8d2f5ab0 | ||
|
|
083ee05d50 | ||
|
|
ed0516d724 | ||
|
|
7535a1ade8 | ||
|
|
8846808804 | ||
|
|
3b6333e65f | ||
|
|
b1148fd735 | ||
|
|
12f7720309 | ||
|
|
2d8530feb3 | ||
|
|
7faeffc5ab | ||
|
|
f81d4bdae7 | ||
|
|
6893773280 | ||
|
|
73a655544e | ||
|
|
baa5a663c4 | ||
|
|
c260fb1c48 | ||
|
|
532f7b9923 | ||
|
|
bb0331ae28 | ||
|
|
8d124713fc | ||
|
|
fb2b362b60 | ||
|
|
75f65fea4f | ||
|
|
812269d656 | ||
|
|
e46cda5228 | ||
|
|
2e1d761854 | ||
|
|
b1e841f168 | ||
|
|
0bb035cfa7 | ||
|
|
3479293416 | ||
|
|
136dac1352 | ||
|
|
e5b713b04a | ||
|
|
3847429d00 | ||
|
|
bb7d33c9d4 | ||
|
|
4598a7c0cc | ||
|
|
b1de196509 | ||
|
|
2a8c650f2c | ||
|
|
f277b128b6 | ||
|
|
4903950b22 | ||
|
|
f55e8d7a11 | ||
|
|
900c9e270e | ||
|
|
a9dc1586a0 | ||
|
|
947caa3599 | ||
|
|
7d1d5c8acd | ||
|
|
ddaf8c3e43 | ||
|
|
dd1a3e7675 | ||
|
|
c17662fe39 | ||
|
|
3a0fb3f89e | ||
|
|
1fcf92e11f | ||
|
|
6a29a50f25 | ||
|
|
75867bd073 | ||
|
|
f44a0bc159 | ||
|
|
07036b5640 | ||
|
|
a7cd4e85cf | ||
|
|
6a9c4f1026 | ||
|
|
427fe6bd82 | ||
|
|
2aa674c1fd | ||
|
|
52bb2b5daf | ||
|
|
db97974dc1 | ||
|
|
b9dbc58e20 | ||
|
|
35f8188410 | ||
|
|
8ffb1f2011 | ||
|
|
26e9c80626 | ||
|
|
f462aaa7b6 | ||
|
|
5a162932f3 | ||
|
|
b2d6b6a70c | ||
|
|
f743e885a3 | ||
|
|
5747f84736 | ||
|
|
879a4cbcd8 | ||
|
|
2449f2555c | ||
|
|
d8f56eee3e | ||
|
|
8d88a0a9af | ||
|
|
845c02cb86 | ||
|
|
64bed506c2 | ||
|
|
ff65f1d006 | ||
|
|
a9f26286f9 | ||
|
|
037ac80a32 | ||
|
|
1bfdfd1f41 | ||
|
|
5202bf03c1 | ||
|
|
387c229f28 | ||
|
|
23e0c9b6e0 | ||
|
|
02ced426fd | ||
|
|
4442535a45 | ||
|
|
edd9f14752 | ||
|
|
ce50cc9523 | ||
|
|
981c54432c | ||
|
|
44c55dd036 | ||
|
|
c72d77065d | ||
|
|
44a2d2214c | ||
|
|
3f5a73793a | ||
|
|
e0b2246c68 | ||
|
|
e0ae8e59bf | ||
|
|
a9641e475a | ||
|
|
05c7505563 | ||
|
|
8561263545 | ||
|
|
a32151525c | ||
|
|
546e7c5da4 | ||
|
|
6fb06a720a | ||
|
|
c54a0713de | ||
|
|
50dc13f280 | ||
|
|
c8ed8dd1a4 | ||
|
|
a807d33600 | ||
|
|
1f1be6fd3d | ||
|
|
c49fe79207 | ||
|
|
f66774e30b | ||
|
|
1281a748d0 | ||
|
|
222acfd070 | ||
|
|
980ecc5f07 | ||
|
|
e8ce73b496 | ||
|
|
f954a30c34 | ||
|
|
60f9cfbb2a | ||
|
|
6822bf2f58 | ||
|
|
2f7f1fa97a | ||
|
|
8c2ce2dedb | ||
|
|
3188ef5731 | ||
|
|
9704beddf8 | ||
|
|
1be54efbeb | ||
|
|
746d983849 | ||
|
|
8d9de76826 | ||
|
|
9488757c29 | ||
|
|
351286486c | ||
|
|
78fcd0237a | ||
|
|
81942d31d6 | ||
|
|
b75b5114c3 | ||
|
|
abcb444dd9 | ||
|
|
983b6904a7 | ||
|
|
3dc2d145ef | ||
|
|
c8f6d37290 | ||
|
|
69dd1a9bd6 | ||
|
|
d93314a683 | ||
|
|
a55e87faaa | ||
|
|
515d0e3fb4 | ||
|
|
22dcc31193 | ||
|
|
909ef0344b | ||
|
|
a2b0e86632 | ||
|
|
d0c1f1a84c | ||
|
|
b62da7e86b | ||
|
|
5e9a3eb6ae | ||
|
|
3451d6874f | ||
|
|
18c3783a1c | ||
|
|
4b46351d36 | ||
|
|
b7c406637d | ||
|
|
c4e5651215 | ||
|
|
23b0e64199 | ||
|
|
fc31b091e4 | ||
|
|
effacf8fc8 | ||
|
|
95130fcfd0 | ||
|
|
5e81105317 | ||
|
|
5b4105e1e6 | ||
|
|
2d3a0d6038 | ||
|
|
fe0b3f459f | ||
|
|
ca69b6577e | ||
|
|
880b863f95 | ||
|
|
78384c3ff5 | ||
|
|
c1c4400c4a | ||
|
|
fc6f974617 | ||
|
|
14b249b804 | ||
|
|
0195824794 | ||
|
|
fb019f15b4 | ||
|
|
abc7fd374b | ||
|
|
cd652dca75 | ||
|
|
c836de44af | ||
|
|
badae2f8fd | ||
|
|
1f34718ecd | ||
|
|
ebda60fd6b | ||
|
|
d242f57758 | ||
|
|
b95e1dda34 | ||
|
|
8f2a97c6e3 | ||
|
|
ebaf25d55a | ||
|
|
42711c23d7 | ||
|
|
f6393728c7 | ||
|
|
d92ab7e8e0 | ||
|
|
5845b3965c | ||
|
|
aacec30ad1 | ||
|
|
2dbdf61c37 | ||
|
|
83365058ce | ||
|
|
3b93c62e23 | ||
|
|
946cccaa1a | ||
|
|
1838d9cd0f | ||
|
|
f62a982a51 | ||
|
|
dfd9dc40ea | ||
|
|
5efea652e3 | ||
|
|
dfa795ff9d | ||
|
|
2cc6b47fcf | ||
|
|
117025a96b | ||
|
|
3e0a9b99ff | ||
|
|
17b3e7e225 | ||
|
|
c065729468 | ||
|
|
55db3ec65c | ||
|
|
0404b78b54 | ||
|
|
68d1bd88b1 | ||
|
|
308b70b039 | ||
|
|
7fa6333a0c | ||
|
|
3279070f9f | ||
|
|
b37669cb3b | ||
|
|
1e538bf73e | ||
|
|
366c032c36 | ||
|
|
95113ad12f | ||
|
|
ce9b2770e2 | ||
|
|
4fc7d76759 | ||
|
|
81bef93e5e | ||
|
|
31d9ed81c5 | ||
|
|
c1af5089b9 | ||
|
|
77efc09362 | ||
|
|
871ca5e4ae | ||
|
|
ceace26ed4 | ||
|
|
75a9a72e78 | ||
|
|
d9d0d1d1a2 | ||
|
|
ea5ce3befb | ||
|
|
e18464b274 | ||
|
|
bd26083f33 | ||
|
|
991363a104 | ||
|
|
a290e58982 | ||
|
|
dcad9724bc | ||
|
|
949d14ae2b | ||
|
|
a6ed4aee84 | ||
|
|
519d7f2b8a | ||
|
|
dddb623a11 | ||
|
|
266cf0622c | ||
|
|
9604e249c9 | ||
|
|
dbc47c9122 | ||
|
|
4c243cbf89 | ||
|
|
deafb7c8b8 | ||
|
|
50309aa295 | ||
|
|
9eaea6a2fd | ||
|
|
830fb2cdb2 | ||
|
|
7cfed73be8 | ||
|
|
41bc04b1c4 | ||
|
|
20cf0094e5 | ||
|
|
83fb4978ad | ||
|
|
51e54dac8b | ||
|
|
c201c15f8c | ||
|
|
0c8196f8b0 | ||
|
|
ee10148444 | ||
|
|
1c95fc2654 | ||
|
|
da69bb4d12 | ||
|
|
0a506bf2e9 | ||
|
|
b2a9ba2ee4 | ||
|
|
f976150b67 | ||
|
|
b1dd8d28bc | ||
|
|
4edeb5ce47 | ||
|
|
d34a8689e5 | ||
|
|
a919d2de56 | ||
|
|
46a8f28b74 | ||
|
|
57398c6df1 | ||
|
|
7affc6878e | ||
|
|
46b2f7eaaf | ||
|
|
9e401b6ef7 | ||
|
|
fe392abeb4 | ||
|
|
f6cc829758 | ||
|
|
6575ee93f2 | ||
|
|
530d03791d | ||
|
|
d40ae8c8ca | ||
|
|
2204614134 | ||
|
|
188ee44f81 | ||
|
|
a4c9aaf337 |
14
.cargo/config.toml
Normal file
14
.cargo/config.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[alias]
|
||||||
|
lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
|
||||||
|
lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo"
|
||||||
|
|
||||||
|
# lib checking
|
||||||
|
ci-check-min = "hack --workspace check --no-default-features"
|
||||||
|
ci-check-default = "hack --workspace check"
|
||||||
|
ci-check-default-tests = "check --workspace --tests"
|
||||||
|
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check"
|
||||||
|
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
|
||||||
|
|
||||||
|
# testing
|
||||||
|
ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture"
|
||||||
|
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
|
||||||
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,15 +1,8 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- 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: Actix Web Q&A
|
|
||||||
- name: Gitter chat (actix)
|
|
||||||
url: https://gitter.im/actix/actix
|
|
||||||
about: Actix (actor framework) Q&A
|
|
||||||
- name: Actix Discord
|
- name: Actix Discord
|
||||||
url: https://discord.gg/NWpN5mmg3x
|
url: https://discord.gg/NWpN5mmg3x
|
||||||
about: Actix developer discussion and community chat
|
about: Actix developer discussion and community chat
|
||||||
|
- name: GitHub Discussions
|
||||||
|
url: https://github.com/actix/actix-web/discussions
|
||||||
|
about: Actix Web Q&A
|
||||||
|
|||||||
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,21 +1,21 @@
|
|||||||
<!-- Thanks for considering contributing actix! -->
|
<!-- Thanks for considering contributing actix! -->
|
||||||
<!-- Please fill out the following to make our reviews easy. -->
|
<!-- Please fill out the following to get your PR reviewed quicker. -->
|
||||||
|
|
||||||
## PR Type
|
## PR Type
|
||||||
<!-- What kind of change does this PR make? -->
|
<!-- What kind of change does this PR make? -->
|
||||||
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
|
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
|
||||||
INSERT_PR_TYPE
|
PR_TYPE
|
||||||
|
|
||||||
|
|
||||||
## PR Checklist
|
## PR Checklist
|
||||||
Check your PR fulfills the following:
|
<!-- Check your PR fulfills the following items. -->
|
||||||
|
|
||||||
<!-- For draft PRs check the boxes as you complete them. -->
|
<!-- For draft PRs check the boxes as you complete them. -->
|
||||||
|
|
||||||
- [ ] Tests for the changes have been added / updated.
|
- [ ] Tests for the changes have been added / updated.
|
||||||
- [ ] Documentation comments have been added / updated.
|
- [ ] Documentation comments have been added / updated.
|
||||||
- [ ] A changelog entry has been made for the appropriate packages.
|
- [ ] A changelog entry has been made for the appropriate packages.
|
||||||
- [ ] Format code with the latest stable rustfmt
|
- [ ] Format code with the latest stable rustfmt.
|
||||||
|
- [ ] (Team) Label with affected crates and semver status.
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|||||||
2
.github/workflows/bench.yml
vendored
2
.github/workflows/bench.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Benchmark (Linux)
|
name: Benchmark
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|||||||
183
.github/workflows/ci.yml
vendored
Normal file
183
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_test:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||||
|
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||||
|
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||||
|
version:
|
||||||
|
- 1.52.0 # MSRV
|
||||||
|
- stable
|
||||||
|
- nightly
|
||||||
|
|
||||||
|
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||||
|
runs-on: ${{ matrix.target.os }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
CI: 1
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
VCPKGRS_DYNAMIC: 1
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# install OpenSSL on Windows
|
||||||
|
# TODO: GitHub actions docs state that OpenSSL is
|
||||||
|
# already installed on these Windows machines somewhere
|
||||||
|
- name: Set vcpkg root
|
||||||
|
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||||
|
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
|
- name: Install OpenSSL
|
||||||
|
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||||
|
run: vcpkg install openssl:x64-windows
|
||||||
|
|
||||||
|
- name: Install ${{ matrix.version }}
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: cargo-hack
|
||||||
|
|
||||||
|
- name: check minimal
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-min }
|
||||||
|
|
||||||
|
- name: check default
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-default }
|
||||||
|
|
||||||
|
- name: tests
|
||||||
|
timeout-minutes: 60
|
||||||
|
run: |
|
||||||
|
cargo test --lib --tests -p=actix-router --all-features
|
||||||
|
cargo test --lib --tests -p=actix-http --all-features
|
||||||
|
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||||
|
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||||
|
cargo test --lib --tests -p=awc --all-features
|
||||||
|
cargo test --lib --tests -p=actix-http-test --all-features
|
||||||
|
cargo test --lib --tests -p=actix-test --all-features
|
||||||
|
cargo test --lib --tests -p=actix-files
|
||||||
|
cargo test --lib --tests -p=actix-multipart --all-features
|
||||||
|
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||||
|
|
||||||
|
- name: tests (io-uring)
|
||||||
|
if: matrix.target.os == 'ubuntu-latest'
|
||||||
|
timeout-minutes: 60
|
||||||
|
run: >
|
||||||
|
sudo bash -c "ulimit -Sl 512
|
||||||
|
&& ulimit -Hl 512
|
||||||
|
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||||
|
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||||
|
|
||||||
|
- name: Clear the cargo caches
|
||||||
|
run: |
|
||||||
|
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||||
|
cargo-cache
|
||||||
|
|
||||||
|
ci_feature_powerset_check:
|
||||||
|
name: Verify Feature Combinations
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install stable
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: cargo-hack
|
||||||
|
|
||||||
|
- name: check feature combinations
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-all-feature-powerset }
|
||||||
|
|
||||||
|
- name: check feature combinations
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-all-feature-powerset-linux }
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
name: coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install stable
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Generate coverage file
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
run: |
|
||||||
|
cargo install cargo-tarpaulin --vers "^0.13"
|
||||||
|
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
||||||
|
- name: Upload to Codecov
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
with: { file: cobertura.xml }
|
||||||
|
|
||||||
|
rustdoc:
|
||||||
|
name: doc tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Rust (nightly)
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.3.0
|
||||||
|
|
||||||
|
- name: doc tests
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
timeout-minutes: 60
|
||||||
|
with: { command: ci-doctest }
|
||||||
21
.github/workflows/clippy-fmt.yml
vendored
21
.github/workflows/clippy-fmt.yml
vendored
@@ -1,32 +1,39 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
name: Clippy and rustfmt Check
|
|
||||||
jobs:
|
jobs:
|
||||||
clippy_check:
|
fmt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
override: true
|
|
||||||
- name: Check with rustfmt
|
- name: Check with rustfmt
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: fmt
|
command: fmt
|
||||||
args: --all -- --check
|
args: --all -- --check
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
clippy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: stable
|
||||||
components: clippy
|
components: clippy
|
||||||
override: true
|
override: true
|
||||||
- name: Check with Clippy
|
- name: Check with Clippy
|
||||||
uses: actions-rs/clippy-check@v1
|
uses: actions-rs/clippy-check@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
args: --all-features --all --tests
|
args: --workspace --all-features --tests
|
||||||
|
|||||||
81
.github/workflows/linux.yml
vendored
81
.github/workflows/linux.yml
vendored
@@ -1,81 +0,0 @@
|
|||||||
name: CI (Linux)
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_test:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
version:
|
|
||||||
- 1.46.0 # MSRV
|
|
||||||
- stable
|
|
||||||
- nightly
|
|
||||||
|
|
||||||
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install ${{ matrix.version }}
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu
|
|
||||||
profile: minimal
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: generate-lockfile
|
|
||||||
- name: Cache Dependencies
|
|
||||||
uses: Swatinem/rust-cache@v1.0.1
|
|
||||||
|
|
||||||
- name: check build
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
args: --all --bins --examples --tests
|
|
||||||
|
|
||||||
- name: tests
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
timeout-minutes: 40
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all --all-features --no-fail-fast -- --nocapture
|
|
||||||
|
|
||||||
- name: tests (actix-http)
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
timeout-minutes: 40
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --package=actix-http --no-default-features --features=rustls -- --nocapture
|
|
||||||
|
|
||||||
- name: tests (awc)
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
timeout-minutes: 40
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --package=awc --no-default-features --features=rustls -- --nocapture
|
|
||||||
|
|
||||||
- name: Generate coverage file
|
|
||||||
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
|
|
||||||
run: |
|
|
||||||
cargo install cargo-tarpaulin --vers "^0.13"
|
|
||||||
cargo tarpaulin --out Xml
|
|
||||||
- name: Upload to Codecov
|
|
||||||
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
|
|
||||||
uses: codecov/codecov-action@v1
|
|
||||||
with:
|
|
||||||
file: cobertura.xml
|
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
|
||||||
run: |
|
|
||||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
56
.github/workflows/macos.yml
vendored
56
.github/workflows/macos.yml
vendored
@@ -1,56 +0,0 @@
|
|||||||
name: CI (macOS)
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_test:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
version:
|
|
||||||
- stable
|
|
||||||
- nightly
|
|
||||||
|
|
||||||
name: ${{ matrix.version }} - x86_64-apple-darwin
|
|
||||||
runs-on: macOS-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install ${{ matrix.version }}
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.version }}-x86_64-apple-darwin
|
|
||||||
profile: minimal
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: generate-lockfile
|
|
||||||
- name: Cache Dependencies
|
|
||||||
uses: Swatinem/rust-cache@v1.0.1
|
|
||||||
|
|
||||||
- name: check build
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
args: --all --bins --examples --tests
|
|
||||||
|
|
||||||
- name: tests
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all --all-features --no-fail-fast -- --nocapture
|
|
||||||
--skip=test_h2_content_length
|
|
||||||
--skip=test_reading_deflate_encoding_large_random_rustls
|
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
|
||||||
run: |
|
|
||||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
12
.github/workflows/upload-doc.yml
vendored
12
.github/workflows/upload-doc.yml
vendored
@@ -1,14 +1,12 @@
|
|||||||
name: Upload documentation
|
name: Upload Documentation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [master]
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'actix/actix-web'
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -20,14 +18,14 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- name: check build
|
- name: Build Docs
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: doc
|
command: doc
|
||||||
args: --no-deps --workspace --all-features
|
args: --workspace --all-features --no-deps
|
||||||
|
|
||||||
- name: Tweak HTML
|
- name: Tweak HTML
|
||||||
run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html
|
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||||
|
|||||||
76
.github/workflows/windows.yml
vendored
76
.github/workflows/windows.yml
vendored
@@ -1,76 +0,0 @@
|
|||||||
name: CI (Windows)
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
env:
|
|
||||||
VCPKGRS_DYNAMIC: 1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_test:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
version:
|
|
||||||
- stable
|
|
||||||
- nightly
|
|
||||||
|
|
||||||
name: ${{ matrix.version }} - x86_64-pc-windows-msvc
|
|
||||||
runs-on: windows-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install ${{ matrix.version }}
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.version }}-x86_64-pc-windows-msvc
|
|
||||||
profile: minimal
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Install OpenSSL
|
|
||||||
run: |
|
|
||||||
vcpkg integrate install
|
|
||||||
vcpkg install openssl:x64-windows
|
|
||||||
Copy-Item C:\vcpkg\installed\x64-windows\bin\libcrypto-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libcrypto.dll
|
|
||||||
Copy-Item C:\vcpkg\installed\x64-windows\bin\libssl-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libssl.dll
|
|
||||||
Get-ChildItem C:\vcpkg\installed\x64-windows\bin
|
|
||||||
Get-ChildItem C:\vcpkg\installed\x64-windows\lib
|
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: generate-lockfile
|
|
||||||
- name: Cache Dependencies
|
|
||||||
uses: Swatinem/rust-cache@v1.0.1
|
|
||||||
|
|
||||||
- name: check build
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
args: --all --bins --examples --tests
|
|
||||||
|
|
||||||
- name: tests
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all --all-features --no-fail-fast -- --nocapture
|
|
||||||
--skip=test_h2_content_length
|
|
||||||
--skip=test_reading_deflate_encoding_large_random_rustls
|
|
||||||
--skip=test_params
|
|
||||||
--skip=test_simple
|
|
||||||
--skip=test_expect_continue
|
|
||||||
--skip=test_http10_keepalive
|
|
||||||
--skip=test_slow_request
|
|
||||||
--skip=test_connection_force_close
|
|
||||||
--skip=test_connection_server_close
|
|
||||||
--skip=test_connection_wait_queue_force_close
|
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
|
||||||
run: |
|
|
||||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,3 +16,6 @@ guide/build/
|
|||||||
|
|
||||||
# Configuration directory generated by CLion
|
# Configuration directory generated by CLion
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# Configuration directory generated by VSCode
|
||||||
|
.vscode
|
||||||
|
|||||||
268
CHANGES.md
268
CHANGES.md
@@ -1,6 +1,254 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
### Added
|
||||||
|
* Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480]
|
||||||
|
* `AcceptEncoding` typed header. [#2482]
|
||||||
|
* `Range` typed header. [#2485]
|
||||||
|
* `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
|
||||||
|
* `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
|
||||||
|
* `HttpServer::on_connect` now receives a `CloneableExtensions` object. [#2327]
|
||||||
|
|
||||||
|
[#2325]: https://github.com/actix/actix-web/pull/2325
|
||||||
|
[#2327]: https://github.com/actix/actix-web/pull/2327
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Rename `Accept::{mime_precedence => ranked}`. [#2480]
|
||||||
|
* Rename `Accept::{mime_preference => preference}`. [#2480]
|
||||||
|
* Un-deprecate `App::data_factory`. [#2484]
|
||||||
|
* `HttpRequest::url_for` no longer constructs URLs with query or fragment components. [#2430]
|
||||||
|
* `HttpServer::on_connect` now receives a `CloneableExtensions` object. [#2327]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Accept wildcard `*` items in `AcceptLanguage`. [#2480]
|
||||||
|
* Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468]
|
||||||
|
* Typed headers containing lists that require one or more items now enforce this minimum. [#2482]
|
||||||
|
|
||||||
|
[#2327]: https://github.com/actix/actix-web/pull/2327
|
||||||
|
[#2430]: https://github.com/actix/actix-web/pull/2430
|
||||||
|
[#2468]: https://github.com/actix/actix-web/pull/2468
|
||||||
|
[#2480]: https://github.com/actix/actix-web/pull/2480
|
||||||
|
[#2482]: https://github.com/actix/actix-web/pull/2482
|
||||||
|
[#2484]: https://github.com/actix/actix-web/pull/2484
|
||||||
|
[#2485]: https://github.com/actix/actix-web/pull/2485
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.13 - 2021-11-30
|
||||||
|
### Changed
|
||||||
|
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||||
|
|
||||||
|
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.12 - 2021-11-22
|
||||||
|
### Changed
|
||||||
|
* Compress middleware's response type is now `AnyBody<Encoder<B>>`. [#2448]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446]
|
||||||
|
|
||||||
|
[#2446]: https://github.com/actix/actix-web/pull/2446
|
||||||
|
[#2448]: https://github.com/actix/actix-web/pull/2448
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.11 - 2021-11-15
|
||||||
|
### Added
|
||||||
|
* Re-export `dev::ServerHandle` from `actix-server`. [#2442]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423]
|
||||||
|
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||||
|
|
||||||
|
[#2423]: https://github.com/actix/actix-web/pull/2423
|
||||||
|
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.10 - 2021-10-20
|
||||||
|
### Added
|
||||||
|
* Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362]
|
||||||
|
* `#[actix_web::test]` macro for setting up tests with a runtime. [#2409]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Associated type `FromRequest::Config` was removed. [#2233]
|
||||||
|
* Inner field made private on `web::Payload`. [#2384]
|
||||||
|
* `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403]
|
||||||
|
* Updated rustls to v0.20. [#2414]
|
||||||
|
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Useless `ServiceResponse::checked_expr` method. [#2401]
|
||||||
|
|
||||||
|
[#2233]: https://github.com/actix/actix-web/pull/2233
|
||||||
|
[#2362]: https://github.com/actix/actix-web/pull/2362
|
||||||
|
[#2384]: https://github.com/actix/actix-web/pull/2384
|
||||||
|
[#2401]: https://github.com/actix/actix-web/pull/2401
|
||||||
|
[#2403]: https://github.com/actix/actix-web/pull/2403
|
||||||
|
[#2409]: https://github.com/actix/actix-web/pull/2409
|
||||||
|
[#2414]: https://github.com/actix/actix-web/pull/2414
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.9 - 2021-09-09
|
||||||
|
### Added
|
||||||
|
* Re-export actix-service `ServiceFactory` in `dev` module. [#2325]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344]
|
||||||
|
* Move `BaseHttpResponse` to `dev::Response`. [#2379]
|
||||||
|
* Enable `TestRequest::param` to accept more than just static strings. [#2172]
|
||||||
|
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Fix quality parse error in Accept-Encoding header. [#2344]
|
||||||
|
* Re-export correct type at `web::HttpResponse`. [#2379]
|
||||||
|
|
||||||
|
[#2172]: https://github.com/actix/actix-web/pull/2172
|
||||||
|
[#2325]: https://github.com/actix/actix-web/pull/2325
|
||||||
|
[#2344]: https://github.com/actix/actix-web/pull/2344
|
||||||
|
[#2379]: https://github.com/actix/actix-web/pull/2379
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.8 - 2021-06-26
|
||||||
|
### Added
|
||||||
|
* Add `ServiceRequest::parts_mut`. [#2177]
|
||||||
|
* Add extractors for `Uri` and `Method`. [#2263]
|
||||||
|
* Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263]
|
||||||
|
* Add `Route::service` for using hand-written services as handlers. [#2262]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Change compression algorithm features flags. [#2250]
|
||||||
|
* Deprecate `App::data` and `App::data_factory`. [#2271]
|
||||||
|
* Smarter extraction of `ConnectionInfo` parts. [#2282]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Scope and Resource middleware can access data items set on their own layer. [#2288]
|
||||||
|
|
||||||
|
[#2177]: https://github.com/actix/actix-web/pull/2177
|
||||||
|
[#2250]: https://github.com/actix/actix-web/pull/2250
|
||||||
|
[#2271]: https://github.com/actix/actix-web/pull/2271
|
||||||
|
[#2262]: https://github.com/actix/actix-web/pull/2262
|
||||||
|
[#2263]: https://github.com/actix/actix-web/pull/2263
|
||||||
|
[#2282]: https://github.com/actix/actix-web/pull/2282
|
||||||
|
[#2288]: https://github.com/actix/actix-web/pull/2288
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.7 - 2021-06-17
|
||||||
|
### Added
|
||||||
|
* `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162]
|
||||||
|
[#2162]: (https://github.com/actix/actix-web/pull/2162)
|
||||||
|
* `ServiceResponse::error_response` now uses body type of `Body`. [#2201]
|
||||||
|
* `ServiceResponse::checked_expr` now returns a `Result`. [#2201]
|
||||||
|
* Update `language-tags` to `0.3`.
|
||||||
|
* `ServiceResponse::take_body`. [#2201]
|
||||||
|
* `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody<B>` types. [#2201]
|
||||||
|
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
|
||||||
|
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
|
||||||
|
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
|
||||||
|
* `middleware::normalize` now will not try to normalize URIs with no valid path [#2246]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201]
|
||||||
|
|
||||||
|
[#2200]: https://github.com/actix/actix-web/pull/2200
|
||||||
|
[#2201]: https://github.com/actix/actix-web/pull/2201
|
||||||
|
[#2253]: https://github.com/actix/actix-web/pull/2253
|
||||||
|
[#2246]: https://github.com/actix/actix-web/pull/2246
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.6 - 2021-04-17
|
||||||
|
### Added
|
||||||
|
* `HttpResponse` and `HttpResponseBuilder` structs. [#2065]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Most error types are now marked `#[non_exhaustive]`. [#2148]
|
||||||
|
* Methods on `ContentDisposition` that took `T: AsRef<str>` now take `impl AsRef<str>`.
|
||||||
|
|
||||||
|
[#2065]: https://github.com/actix/actix-web/pull/2065
|
||||||
|
[#2148]: https://github.com/actix/actix-web/pull/2148
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.5 - 2021-04-02
|
||||||
|
### Added
|
||||||
|
* `Header` extractor for extracting common HTTP headers in handlers. [#2094]
|
||||||
|
* Added `TestServer::client_headers` method. [#2097]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Double ampersand in Logger format is escaped correctly. [#2067]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed
|
||||||
|
instead of skipping. (Only the first error is kept when multiple error occur) [#2093]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* The `client` mod was removed. Clients should now use `awc` directly.
|
||||||
|
[871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7)
|
||||||
|
* Integration testing was moved to new `actix-test` crate. Namely these items from the `test`
|
||||||
|
module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112]
|
||||||
|
|
||||||
|
[#2067]: https://github.com/actix/actix-web/pull/2067
|
||||||
|
[#2093]: https://github.com/actix/actix-web/pull/2093
|
||||||
|
[#2094]: https://github.com/actix/actix-web/pull/2094
|
||||||
|
[#2097]: https://github.com/actix/actix-web/pull/2097
|
||||||
|
[#2112]: https://github.com/actix/actix-web/pull/2112
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.4 - 2021-03-09
|
||||||
|
### Changed
|
||||||
|
* Feature `cookies` is now optional and enabled by default. [#1981]
|
||||||
|
* `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default
|
||||||
|
behaviour of the `web::Json<T>` extractor. [#2010]
|
||||||
|
|
||||||
|
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||||
|
[#2010]: https://github.com/actix/actix-web/pull/2010
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.3 - 2021-02-10
|
||||||
|
* Update `actix-web-codegen` to `0.5.0-beta.1`.
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0-beta.2 - 2021-02-10
|
||||||
|
### Added
|
||||||
|
* The method `Either<web::Json<T>, web::Form<T>>::into_inner()` which returns the inner type for
|
||||||
|
whichever variant was created. Also works for `Either<web::Form<T>, web::Json<T>>`. [#1894]
|
||||||
|
* Add `services!` macro for helping register multiple services to `App`. [#1933]
|
||||||
|
* Enable registering a vec of services of the same type to `App` [#1933]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly.
|
||||||
|
Making it simpler and more performant. [#1891]
|
||||||
|
* `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893]
|
||||||
|
* `ServiceRequest::from_request` can no longer fail. [#1893]
|
||||||
|
* Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894]
|
||||||
|
* `test::{call_service, read_response, read_response_json, send_request}` take `&Service`
|
||||||
|
in argument [#1905]
|
||||||
|
* `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure
|
||||||
|
argument. [#1905]
|
||||||
|
* `web::block` no longer requires the output is a Result. [#1957]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Public field of `web::Path` has been made private. [#1894]
|
||||||
|
* Public field of `web::Query` has been made private. [#1894]
|
||||||
|
* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
|
||||||
|
* `AppService::set_service_data`; for custom HTTP service factories adding application data, use the
|
||||||
|
layered data model by calling `ServiceRequest::add_data_container` when handling
|
||||||
|
requests instead. [#1906]
|
||||||
|
|
||||||
|
[#1891]: https://github.com/actix/actix-web/pull/1891
|
||||||
|
[#1893]: https://github.com/actix/actix-web/pull/1893
|
||||||
|
[#1894]: https://github.com/actix/actix-web/pull/1894
|
||||||
|
[#1869]: https://github.com/actix/actix-web/pull/1869
|
||||||
|
[#1905]: https://github.com/actix/actix-web/pull/1905
|
||||||
|
[#1906]: https://github.com/actix/actix-web/pull/1906
|
||||||
|
[#1933]: https://github.com/actix/actix-web/pull/1933
|
||||||
|
[#1957]: https://github.com/actix/actix-web/pull/1957
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.1 - 2021-01-07
|
## 4.0.0-beta.1 - 2021-01-07
|
||||||
@@ -24,13 +272,15 @@
|
|||||||
### Removed
|
### Removed
|
||||||
* Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now
|
* Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now
|
||||||
exposed directly by the `middleware` module.
|
exposed directly by the `middleware` module.
|
||||||
|
* Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported
|
||||||
|
from `actix_web::error` module. [#1878]
|
||||||
|
|
||||||
[#1812]: https://github.com/actix/actix-web/pull/1812
|
[#1812]: https://github.com/actix/actix-web/pull/1812
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
[#1852]: https://github.com/actix/actix-web/pull/1852
|
[#1852]: https://github.com/actix/actix-web/pull/1852
|
||||||
[#1865]: https://github.com/actix/actix-web/pull/1865
|
[#1865]: https://github.com/actix/actix-web/pull/1865
|
||||||
[#1875]: https://github.com/actix/actix-web/pull/1875
|
[#1875]: https://github.com/actix/actix-web/pull/1875
|
||||||
|
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||||
|
|
||||||
## 3.3.2 - 2020-12-01
|
## 3.3.2 - 2020-12-01
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -115,7 +365,7 @@
|
|||||||
|
|
||||||
## 3.0.0-beta.4 - 2020-09-09
|
## 3.0.0-beta.4 - 2020-09-09
|
||||||
### Added
|
### Added
|
||||||
* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing
|
* `middleware::NormalizePath` now has configurable behavior for either always having a trailing
|
||||||
slash, or as the new addition, always trimming trailing slashes. [#1639]
|
slash, or as the new addition, always trimming trailing slashes. [#1639]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -443,7 +693,7 @@
|
|||||||
|
|
||||||
## [1.0.0-rc] - 2019-05-18
|
## [1.0.0-rc] - 2019-05-18
|
||||||
|
|
||||||
### Add
|
### Added
|
||||||
|
|
||||||
* Add `Query<T>::from_query()` to extract parameters from a query string. #846
|
* Add `Query<T>::from_query()` to extract parameters from a query string. #846
|
||||||
* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
|
* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
|
||||||
@@ -459,7 +709,7 @@
|
|||||||
|
|
||||||
## [1.0.0-beta.4] - 2019-05-12
|
## [1.0.0-beta.4] - 2019-05-12
|
||||||
|
|
||||||
### Add
|
### Added
|
||||||
|
|
||||||
* Allow to set/override app data on scope level
|
* Allow to set/override app data on scope level
|
||||||
|
|
||||||
@@ -485,7 +735,7 @@
|
|||||||
|
|
||||||
* CORS handling without headers #702
|
* CORS handling without headers #702
|
||||||
|
|
||||||
* Allow to construct `Data` instances to avoid double `Arc` for `Send + Sync` types.
|
* Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@@ -549,7 +799,7 @@
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Allow to use any service as default service.
|
* Allow using any service as default service.
|
||||||
|
|
||||||
* Remove generic type for request payload, always use default.
|
* Remove generic type for request payload, always use default.
|
||||||
|
|
||||||
@@ -612,13 +862,13 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* rustls support
|
* Rustls support
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* use forked cookie
|
* Use forked cookie
|
||||||
|
|
||||||
* multipart::Field renamed to MultipartField
|
* Multipart::Field renamed to MultipartField
|
||||||
|
|
||||||
## [1.0.0-alpha.1] - 2019-03-28
|
## [1.0.0-alpha.1] - 2019-03-28
|
||||||
|
|
||||||
|
|||||||
184
Cargo.toml
184
Cargo.toml
@@ -1,121 +1,130 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web"
|
name = "actix-web"
|
||||||
version = "4.0.0-beta.1"
|
version = "4.0.0-beta.13"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||||
readme = "README.md"
|
|
||||||
keywords = ["actix", "http", "web", "framework", "async"]
|
keywords = ["actix", "http", "web", "framework", "async"]
|
||||||
|
categories = [
|
||||||
|
"network-programming",
|
||||||
|
"asynchronous",
|
||||||
|
"web-programming::http-server",
|
||||||
|
"web-programming::websocket"
|
||||||
|
]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-web.git"
|
repository = "https://github.com/actix/actix-web.git"
|
||||||
documentation = "https://docs.rs/actix-web/"
|
|
||||||
categories = ["network-programming", "asynchronous",
|
|
||||||
"web-programming::http-server",
|
|
||||||
"web-programming::websocket"]
|
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["openssl", "rustls", "compress", "secure-cookies"]
|
# features that docs.rs will build with
|
||||||
|
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
|
||||||
[badges]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
travis-ci = { repository = "actix/actix-web", branch = "master" }
|
|
||||||
codecov = { repository = "actix/actix-web", branch = "master", service = "github" }
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_web"
|
name = "actix_web"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
".",
|
".",
|
||||||
"awc",
|
"awc",
|
||||||
"actix-http",
|
"actix-http",
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
"actix-web-codegen",
|
"actix-web-codegen",
|
||||||
"actix-http-test",
|
"actix-http-test",
|
||||||
|
"actix-test",
|
||||||
|
"actix-router",
|
||||||
]
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["compress"]
|
default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
||||||
|
|
||||||
# content-encoding support
|
# Brotli algorithm content-encoding support
|
||||||
compress = ["actix-http/compress", "awc/compress"]
|
compress-brotli = ["actix-http/compress-brotli", "__compress"]
|
||||||
|
# Gzip and deflate algorithms content-encoding support
|
||||||
|
compress-gzip = ["actix-http/compress-gzip", "__compress"]
|
||||||
|
# Zstd algorithm content-encoding support
|
||||||
|
compress-zstd = ["actix-http/compress-zstd", "__compress"]
|
||||||
|
|
||||||
# sessions feature
|
# support for cookies
|
||||||
secure-cookies = ["actix-http/secure-cookies"]
|
cookies = ["cookie"]
|
||||||
|
|
||||||
|
# secure cookies feature
|
||||||
|
secure-cookies = ["cookie/secure"]
|
||||||
|
|
||||||
# openssl
|
# openssl
|
||||||
openssl = ["actix-tls/accept", "actix-tls/openssl", "awc/openssl", "open-ssl"]
|
openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
|
||||||
|
|
||||||
# rustls
|
# rustls
|
||||||
rustls = ["actix-tls/accept", "actix-tls/rustls", "awc/rustls", "rust-tls"]
|
rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
|
||||||
|
|
||||||
[[example]]
|
# Internal (PRIVATE!) features used to aid testing and checking feature status.
|
||||||
name = "basic"
|
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||||
required-features = ["compress"]
|
__compress = []
|
||||||
|
|
||||||
[[example]]
|
# io-uring feature only avaiable for Linux OSes.
|
||||||
name = "uds"
|
experimental-io-uring = ["actix-server/io-uring"]
|
||||||
required-features = ["compress"]
|
|
||||||
|
|
||||||
[[test]]
|
|
||||||
name = "test_server"
|
|
||||||
required-features = ["compress"]
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "on_connect"
|
|
||||||
required-features = []
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "client"
|
|
||||||
required-features = ["rustls"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.4.0-beta.1"
|
actix-codec = "0.4.1"
|
||||||
actix-macros = "0.1.0"
|
actix-macros = "0.2.3"
|
||||||
actix-router = "0.2.4"
|
actix-rt = "2.3"
|
||||||
actix-rt = "2.0.0-beta.1"
|
actix-server = "2.0.0-rc.1"
|
||||||
actix-server = "2.0.0-beta.2"
|
actix-service = "2.0.0"
|
||||||
actix-service = "2.0.0-beta.2"
|
actix-utils = "3.0.0"
|
||||||
actix-utils = "3.0.0-beta.1"
|
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
|
||||||
actix-threadpool = "0.3.1"
|
|
||||||
actix-tls = { version = "3.0.0-beta.2", default-features = false, optional = true }
|
|
||||||
|
|
||||||
actix-web-codegen = "0.4.0"
|
actix-http = "3.0.0-beta.14"
|
||||||
actix-http = "3.0.0-beta.1"
|
actix-router = "0.5.0-beta.2"
|
||||||
awc = { version = "3.0.0-beta.1", default-features = false }
|
actix-web-codegen = "0.5.0-beta.5"
|
||||||
|
|
||||||
ahash = "0.6"
|
ahash = "0.7"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
cfg-if = "1"
|
||||||
|
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
|
either = "1.5.3"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
|
itoa = "0.4"
|
||||||
|
language-tags = "0.3"
|
||||||
|
once_cell = "1.5"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
socket2 = "0.3.16"
|
paste = "1"
|
||||||
pin-project = "1.0.0"
|
pin-project-lite = "0.2.7"
|
||||||
regex = "1.4"
|
regex = "1.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
smallvec = "1.6.1"
|
||||||
|
socket2 = "0.4.0"
|
||||||
|
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
open-ssl = { package = "openssl", version = "0.10", optional = true }
|
|
||||||
rust-tls = { package = "rustls", version = "0.19.0", optional = true }
|
|
||||||
smallvec = "1.6"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix = "0.11.0-beta.1"
|
actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] }
|
||||||
actix-http = { version = "3.0.0-beta.1", features = ["actors"] }
|
awc = { version = "3.0.0-beta.11", features = ["openssl"] }
|
||||||
rand = "0.8"
|
|
||||||
env_logger = "0.8"
|
|
||||||
serde_derive = "1.0"
|
|
||||||
brotli2 = "0.3.2"
|
brotli2 = "0.3.2"
|
||||||
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
|
env_logger = "0.9"
|
||||||
flate2 = "1.0.13"
|
flate2 = "1.0.13"
|
||||||
criterion = "0.3"
|
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||||
|
rand = "0.8"
|
||||||
|
rcgen = "0.8"
|
||||||
|
rustls-pemfile = "0.2"
|
||||||
|
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||||
|
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||||
|
zstd = "0.9"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
|
||||||
|
debug = 0
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
@@ -123,15 +132,42 @@ opt-level = 3
|
|||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
actix-web = { path = "." }
|
actix-files = { path = "actix-files" }
|
||||||
actix-http = { path = "actix-http" }
|
actix-http = { path = "actix-http" }
|
||||||
actix-http-test = { path = "actix-http-test" }
|
actix-http-test = { path = "actix-http-test" }
|
||||||
|
actix-multipart = { path = "actix-multipart" }
|
||||||
|
actix-router = { path = "actix-router" }
|
||||||
|
actix-test = { path = "actix-test" }
|
||||||
|
actix-web = { path = "." }
|
||||||
actix-web-actors = { path = "actix-web-actors" }
|
actix-web-actors = { path = "actix-web-actors" }
|
||||||
actix-web-codegen = { path = "actix-web-codegen" }
|
actix-web-codegen = { path = "actix-web-codegen" }
|
||||||
actix-multipart = { path = "actix-multipart" }
|
|
||||||
actix-files = { path = "actix-files" }
|
|
||||||
awc = { path = "awc" }
|
awc = { path = "awc" }
|
||||||
|
|
||||||
|
# uncomment for quick testing against local actix-net repo
|
||||||
|
# actix-service = { path = "../actix-net/actix-service" }
|
||||||
|
# actix-macros = { path = "../actix-net/actix-macros" }
|
||||||
|
# actix-rt = { path = "../actix-net/actix-rt" }
|
||||||
|
# actix-codec = { path = "../actix-net/actix-codec" }
|
||||||
|
# actix-utils = { path = "../actix-net/actix-utils" }
|
||||||
|
# actix-tls = { path = "../actix-net/actix-tls" }
|
||||||
|
# actix-server = { path = "../actix-net/actix-server" }
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "test_server"
|
||||||
|
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "basic"
|
||||||
|
required-features = ["compress-gzip"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "uds"
|
||||||
|
required-features = ["compress-gzip"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "on_connect"
|
||||||
|
required-features = []
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "server"
|
name = "server"
|
||||||
harness = false
|
harness = false
|
||||||
@@ -139,3 +175,7 @@ harness = false
|
|||||||
[[bench]]
|
[[bench]]
|
||||||
name = "service"
|
name = "service"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "responder"
|
||||||
|
harness = false
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2017 Actix Team
|
Copyright (c) 2017-NOW Actix Team
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any
|
Permission is hereby granted, free of charge, to any
|
||||||
person obtaining a copy of this software and associated
|
person obtaining a copy of this software and associated
|
||||||
|
|||||||
21
MIGRATION.md
21
MIGRATION.md
@@ -3,13 +3,28 @@
|
|||||||
* The default `NormalizePath` behavior now strips trailing slashes by default. This was
|
* 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
|
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
|
routes defined with trailing slashes will become inaccessible when
|
||||||
using `NormalizePath::default()`.
|
using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning.
|
||||||
|
It is advised that the `new` method be used instead.
|
||||||
|
|
||||||
Before: `#[get("/test/")`
|
Before: `#[get("/test/")]`
|
||||||
After: `#[get("/test")`
|
After: `#[get("/test")]`
|
||||||
|
|
||||||
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
|
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
|
||||||
|
|
||||||
|
* The `type Config` of `FromRequest` was removed.
|
||||||
|
|
||||||
|
* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
|
||||||
|
By default all compression algorithms are enabled.
|
||||||
|
To select algorithm you want to include with `middleware::Compress` use following flags:
|
||||||
|
- `compress-brotli`
|
||||||
|
- `compress-gzip`
|
||||||
|
- `compress-zstd`
|
||||||
|
If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want
|
||||||
|
to have compression enabled. Please change features selection like bellow:
|
||||||
|
|
||||||
|
Before: `"compress"`
|
||||||
|
After: `"compress-brotli", "compress-gzip", "compress-zstd"`
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0
|
## 3.0.0
|
||||||
|
|
||||||
|
|||||||
52
README.md
52
README.md
@@ -1,20 +1,19 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<h1>Actix web</h1>
|
<h1>Actix Web</h1>
|
||||||
<p>
|
<p>
|
||||||
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
|
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web)
|
[](https://crates.io/crates/actix-web)
|
||||||
[](https://docs.rs/actix-web/3.3.2)
|
[](https://docs.rs/actix-web/4.0.0-beta.13)
|
||||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-web/3.3.2)
|
[](https://deps.rs/crate/actix-web/4.0.0-beta.13)
|
||||||
<br />
|
<br />
|
||||||
[](https://travis-ci.org/actix/actix-web)
|
[](https://github.com/actix/actix-web/actions)
|
||||||
[](https://codecov.io/gh/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)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
@@ -26,15 +25,14 @@
|
|||||||
* Streaming and pipelining
|
* Streaming and pipelining
|
||||||
* Keep-alive and slow requests handling
|
* Keep-alive and slow requests handling
|
||||||
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
||||||
* Transparent content compression/decompression (br, gzip, deflate)
|
* Transparent content compression/decompression (br, gzip, deflate, zstd)
|
||||||
* Powerful [request routing](https://actix.rs/docs/url-dispatch/)
|
* Powerful [request routing](https://actix.rs/docs/url-dispatch/)
|
||||||
* Multipart streams
|
* Multipart streams
|
||||||
* Static assets
|
* Static assets
|
||||||
* SSL support using OpenSSL or Rustls
|
* SSL support using OpenSSL or Rustls
|
||||||
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||||
* Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
|
* Includes an async [HTTP client](https://docs.rs/awc/)
|
||||||
* Supports [Actix actor framework](https://github.com/actix/actix)
|
* Runs on stable Rust 1.52+
|
||||||
* Runs on stable Rust 1.46+
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -73,18 +71,18 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
### More examples
|
### More examples
|
||||||
|
|
||||||
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/)
|
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/)
|
||||||
* [Application State](https://github.com/actix/examples/tree/master/state/)
|
* [Application State](https://github.com/actix/examples/tree/master/basics/state/)
|
||||||
* [JSON Handling](https://github.com/actix/examples/tree/master/json/)
|
* [JSON Handling](https://github.com/actix/examples/tree/master/json/json/)
|
||||||
* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/)
|
* [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/)
|
||||||
* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/)
|
* [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/)
|
||||||
* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/)
|
* [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/)
|
||||||
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/)
|
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/)
|
||||||
* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/)
|
* [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/)
|
||||||
* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/)
|
* [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/)
|
||||||
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/)
|
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/)
|
||||||
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/)
|
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
|
||||||
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/)
|
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
|
||||||
|
|
||||||
You may consider checking out
|
You may consider checking out
|
||||||
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
||||||
@@ -92,16 +90,16 @@ You may consider checking out
|
|||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
One of the fastest web frameworks available according to the
|
One of the fastest web frameworks available according to the
|
||||||
[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19).
|
[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under either of
|
This project is licensed under either of
|
||||||
|
|
||||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||||
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
[http://www.apache.org/licenses/LICENSE-2.0])
|
||||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||||
[http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
[http://opensource.org/licenses/MIT])
|
||||||
|
|
||||||
at your option.
|
at your option.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,64 @@
|
|||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.9 - 2021-11-22
|
||||||
|
* Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
|
||||||
|
* Add `NamedFile::open_async`. [#2408]
|
||||||
|
* Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
|
||||||
|
* The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408]
|
||||||
|
* The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408]
|
||||||
|
* Add `impl Clone` for `FilesService`. [#2408]
|
||||||
|
|
||||||
|
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||||
|
[#2453]: https://github.com/actix/actix-web/pull/2453
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.8 - 2021-10-20
|
||||||
|
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.7 - 2021-09-09
|
||||||
|
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.6 - 2021-06-26
|
||||||
|
* Added `Files::path_filter()`. [#2274]
|
||||||
|
* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
|
||||||
|
|
||||||
|
[#2274]: https://github.com/actix/actix-web/pull/2274
|
||||||
|
[#2228]: https://github.com/actix/actix-web/pull/2228
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.5 - 2021-06-17
|
||||||
|
* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
|
||||||
|
* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
|
||||||
|
* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
|
||||||
|
* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
|
||||||
|
|
||||||
|
[#2135]: https://github.com/actix/actix-web/pull/2135
|
||||||
|
[#2156]: https://github.com/actix/actix-web/pull/2156
|
||||||
|
[#2225]: https://github.com/actix/actix-web/pull/2225
|
||||||
|
[#2257]: https://github.com/actix/actix-web/pull/2257
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.4 - 2021-04-02
|
||||||
|
* Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
|
||||||
|
|
||||||
|
[#2046]: https://github.com/actix/actix-web/pull/2046
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.3 - 2021-03-09
|
||||||
|
* No notable changes.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0-beta.2 - 2021-02-10
|
||||||
|
* Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
|
||||||
|
* Replace `v_htmlescape` with `askama_escape`. [#1953]
|
||||||
|
|
||||||
|
[#1887]: https://github.com/actix/actix-web/pull/1887
|
||||||
|
[#1953]: https://github.com/actix/actix-web/pull/1953
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.1 - 2021-01-07
|
## 0.6.0-beta.1 - 2021-01-07
|
||||||
* `HttpRange::parse` now has its own error type.
|
* `HttpRange::parse` now has its own error type.
|
||||||
* Update `bytes` to `1.0`. [#1813]
|
* Update `bytes` to `1.0`. [#1813]
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-files"
|
name = "actix-files"
|
||||||
version = "0.6.0-beta.1"
|
version = "0.6.0-beta.9"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = [
|
||||||
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"fakeshadow <24548779@qq.com>",
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
description = "Static file serving for Actix Web"
|
description = "Static file serving for Actix Web"
|
||||||
readme = "README.md"
|
|
||||||
keywords = ["actix", "http", "async", "futures"]
|
keywords = ["actix", "http", "async", "futures"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-web.git"
|
repository = "https://github.com/actix/actix-web"
|
||||||
documentation = "https://docs.rs/actix-files/"
|
|
||||||
categories = ["asynchronous", "web-programming::http-server"]
|
categories = ["asynchronous", "web-programming::http-server"]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -16,20 +18,30 @@ edition = "2018"
|
|||||||
name = "actix_files"
|
name = "actix_files"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "4.0.0-beta.1", default-features = false }
|
actix-web = { version = "4.0.0-beta.11", default-features = false }
|
||||||
actix-service = "2.0.0-beta.2"
|
actix-http = "3.0.0-beta.14"
|
||||||
|
actix-service = "2"
|
||||||
|
actix-utils = "3"
|
||||||
|
|
||||||
|
askama_escape = "0.10"
|
||||||
bitflags = "1"
|
bitflags = "1"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
http-range = "0.1.4"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
mime_guess = "2.0.1"
|
mime_guess = "2.0.1"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
v_htmlescape = "0.12"
|
pin-project-lite = "0.2.7"
|
||||||
|
|
||||||
|
tokio-uring = { version = "0.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.0.0-beta.1"
|
actix-rt = "2.2"
|
||||||
actix-web = "4.0.0-beta.1"
|
actix-web = "4.0.0-beta.11"
|
||||||
|
actix-test = "0.1.0-beta.7"
|
||||||
|
|||||||
@@ -3,17 +3,16 @@
|
|||||||
> Static file serving for Actix Web
|
> Static file serving for Actix Web
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://docs.rs/actix-files/0.5.0)
|
[](https://docs.rs/actix-files/0.6.0-beta.9)
|
||||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-files/0.5.0)
|
[](https://deps.rs/crate/actix-files/0.6.0-beta.9)
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-files/)
|
- [API Documentation](https://docs.rs/actix-files/)
|
||||||
- [Example Project](https://github.com/actix/examples/tree/master/static_index)
|
- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index)
|
||||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
- Minimum Supported Rust Version (MSRV): 1.52
|
||||||
- Minimum supported Rust version: 1.46 or later
|
|
||||||
|
|||||||
@@ -1,94 +1,277 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp, fmt,
|
cmp, fmt,
|
||||||
fs::File,
|
|
||||||
future::Future,
|
future::Future,
|
||||||
io::{self, Read, Seek},
|
io,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_web::{
|
use actix_web::{error::Error, web::Bytes};
|
||||||
error::{BlockingError, Error},
|
|
||||||
web,
|
|
||||||
};
|
|
||||||
use bytes::Bytes;
|
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
use futures_util::future::{FutureExt, LocalBoxFuture};
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
use crate::handle_error;
|
use super::named::File;
|
||||||
|
|
||||||
type ChunkedBoxFuture =
|
pin_project! {
|
||||||
LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>;
|
/// Adapter to read a `std::file::File` in chunks.
|
||||||
|
#[doc(hidden)]
|
||||||
#[doc(hidden)]
|
pub struct ChunkedReadFile<F, Fut> {
|
||||||
/// A helper created from a `std::fs::File` which reads the file
|
size: u64,
|
||||||
/// chunk-by-chunk on a `ThreadPool`.
|
offset: u64,
|
||||||
pub struct ChunkedReadFile {
|
#[pin]
|
||||||
pub(crate) size: u64,
|
state: ChunkedReadFileState<Fut>,
|
||||||
pub(crate) offset: u64,
|
counter: u64,
|
||||||
pub(crate) file: Option<File>,
|
callback: F,
|
||||||
pub(crate) fut: Option<ChunkedBoxFuture>,
|
}
|
||||||
pub(crate) counter: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for ChunkedReadFile {
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
pin_project! {
|
||||||
|
#[project = ChunkedReadFileStateProj]
|
||||||
|
#[project_replace = ChunkedReadFileStateProjReplace]
|
||||||
|
enum ChunkedReadFileState<Fut> {
|
||||||
|
File { file: Option<File>, },
|
||||||
|
Future { #[pin] fut: Fut },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
pin_project! {
|
||||||
|
#[project = ChunkedReadFileStateProj]
|
||||||
|
#[project_replace = ChunkedReadFileStateProjReplace]
|
||||||
|
enum ChunkedReadFileState<Fut> {
|
||||||
|
File { file: Option<(File, BytesMut)> },
|
||||||
|
Future { #[pin] fut: Fut },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, Fut> fmt::Debug for ChunkedReadFile<F, Fut> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str("ChunkedReadFile")
|
f.write_str("ChunkedReadFile")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for ChunkedReadFile {
|
pub(crate) fn new_chunked_read(
|
||||||
|
size: u64,
|
||||||
|
offset: u64,
|
||||||
|
file: File,
|
||||||
|
) -> impl Stream<Item = Result<Bytes, Error>> {
|
||||||
|
ChunkedReadFile {
|
||||||
|
size,
|
||||||
|
offset,
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
state: ChunkedReadFileState::File { file: Some(file) },
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
state: ChunkedReadFileState::File {
|
||||||
|
file: Some((file, BytesMut::new())),
|
||||||
|
},
|
||||||
|
counter: 0,
|
||||||
|
callback: chunked_read_file_callback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
async fn chunked_read_file_callback(
|
||||||
|
mut file: File,
|
||||||
|
offset: u64,
|
||||||
|
max_bytes: usize,
|
||||||
|
) -> Result<(File, Bytes), Error> {
|
||||||
|
use io::{Read as _, Seek as _};
|
||||||
|
|
||||||
|
let res = actix_web::rt::task::spawn_blocking(move || {
|
||||||
|
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 {
|
||||||
|
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
|
||||||
|
} else {
|
||||||
|
Ok((file, Bytes::from(buf)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| actix_web::error::BlockingError)??;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
async fn chunked_read_file_callback(
|
||||||
|
file: File,
|
||||||
|
offset: u64,
|
||||||
|
max_bytes: usize,
|
||||||
|
mut bytes_mut: BytesMut,
|
||||||
|
) -> io::Result<(File, Bytes, BytesMut)> {
|
||||||
|
bytes_mut.reserve(max_bytes);
|
||||||
|
|
||||||
|
let (res, mut bytes_mut) = file.read_at(bytes_mut, offset).await;
|
||||||
|
let n_bytes = res?;
|
||||||
|
|
||||||
|
if n_bytes == 0 {
|
||||||
|
return Err(io::ErrorKind::UnexpectedEof.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = bytes_mut.split_to(n_bytes).freeze();
|
||||||
|
|
||||||
|
Ok((file, bytes, bytes_mut))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||||
|
where
|
||||||
|
F: Fn(File, u64, usize, BytesMut) -> Fut,
|
||||||
|
Fut: Future<Output = io::Result<(File, Bytes, BytesMut)>>,
|
||||||
|
{
|
||||||
type Item = Result<Bytes, Error>;
|
type Item = Result<Bytes, Error>;
|
||||||
|
|
||||||
fn poll_next(
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
mut self: Pin<&mut Self>,
|
let mut this = self.as_mut().project();
|
||||||
cx: &mut Context<'_>,
|
match this.state.as_mut().project() {
|
||||||
) -> Poll<Option<Self::Item>> {
|
ChunkedReadFileStateProj::File { file } => {
|
||||||
if let Some(ref mut fut) = self.fut {
|
let size = *this.size;
|
||||||
return match ready!(Pin::new(fut).poll(cx)) {
|
let offset = *this.offset;
|
||||||
Ok((file, bytes)) => {
|
let counter = *this.counter;
|
||||||
self.fut.take();
|
|
||||||
self.file = Some(file);
|
|
||||||
|
|
||||||
self.offset += bytes.len() as u64;
|
if size == counter {
|
||||||
self.counter += bytes.len() as u64;
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||||
|
|
||||||
Poll::Ready(Some(Ok(bytes)))
|
let (file, bytes_mut) = file
|
||||||
|
.take()
|
||||||
|
.expect("ChunkedReadFile polled after completion");
|
||||||
|
|
||||||
|
let fut = (this.callback)(file, offset, max_bytes, bytes_mut);
|
||||||
|
|
||||||
|
this.state
|
||||||
|
.project_replace(ChunkedReadFileState::Future { fut });
|
||||||
|
|
||||||
|
self.poll_next(cx)
|
||||||
}
|
}
|
||||||
Err(e) => Poll::Ready(Some(Err(handle_error(e)))),
|
}
|
||||||
};
|
ChunkedReadFileStateProj::Future { fut } => {
|
||||||
}
|
let (file, bytes, bytes_mut) = ready!(fut.poll(cx))?;
|
||||||
|
|
||||||
let size = self.size;
|
this.state.project_replace(ChunkedReadFileState::File {
|
||||||
let offset = self.offset;
|
file: Some((file, bytes_mut)),
|
||||||
let counter = self.counter;
|
});
|
||||||
|
|
||||||
if size == counter {
|
*this.offset += bytes.len() as u64;
|
||||||
Poll::Ready(None)
|
*this.counter += bytes.len() as u64;
|
||||||
} else {
|
|
||||||
let mut file = self.file.take().expect("Use after completion");
|
|
||||||
|
|
||||||
self.fut = Some(
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
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))?;
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||||
let n_bytes =
|
where
|
||||||
file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
F: Fn(File, u64, usize) -> Fut,
|
||||||
|
Fut: Future<Output = Result<(File, Bytes), Error>>,
|
||||||
if n_bytes == 0 {
|
{
|
||||||
return Err(io::ErrorKind::UnexpectedEof.into());
|
type Item = Result<Bytes, Error>;
|
||||||
}
|
|
||||||
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
Ok((file, Bytes::from(buf)))
|
let mut this = self.as_mut().project();
|
||||||
})
|
match this.state.as_mut().project() {
|
||||||
.boxed_local(),
|
ChunkedReadFileStateProj::File { file } => {
|
||||||
);
|
let size = *this.size;
|
||||||
|
let offset = *this.offset;
|
||||||
self.poll_next(cx)
|
let counter = *this.counter;
|
||||||
|
|
||||||
|
if size == counter {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||||
|
|
||||||
|
let file = file
|
||||||
|
.take()
|
||||||
|
.expect("ChunkedReadFile polled after completion");
|
||||||
|
|
||||||
|
let fut = (this.callback)(file, offset, max_bytes);
|
||||||
|
|
||||||
|
this.state
|
||||||
|
.project_replace(ChunkedReadFileState::Future { fut });
|
||||||
|
|
||||||
|
self.poll_next(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChunkedReadFileStateProj::Future { fut } => {
|
||||||
|
let (file, bytes) = ready!(fut.poll(cx))?;
|
||||||
|
|
||||||
|
this.state
|
||||||
|
.project_replace(ChunkedReadFileState::File { file: Some(file) });
|
||||||
|
|
||||||
|
*this.offset += bytes.len() as u64;
|
||||||
|
*this.counter += bytes.len() as u64;
|
||||||
|
|
||||||
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
use bytes_mut::BytesMut;
|
||||||
|
|
||||||
|
// TODO: remove new type and use bytes::BytesMut directly
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
mod bytes_mut {
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use tokio_uring::buf::{IoBuf, IoBufMut};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BytesMut(bytes::BytesMut);
|
||||||
|
|
||||||
|
impl BytesMut {
|
||||||
|
pub(super) fn new() -> Self {
|
||||||
|
Self(bytes::BytesMut::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for BytesMut {
|
||||||
|
type Target = bytes::BytesMut;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for BytesMut {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl IoBuf for BytesMut {
|
||||||
|
fn stable_ptr(&self) -> *const u8 {
|
||||||
|
self.0.as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_init(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_total(&self) -> usize {
|
||||||
|
self.0.capacity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl IoBufMut for BytesMut {
|
||||||
|
fn stable_mut_ptr(&mut self) -> *mut u8 {
|
||||||
|
self.0.as_mut_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn set_init(&mut self, init_len: usize) {
|
||||||
|
if self.len() < init_len {
|
||||||
|
self.0.set_len(init_len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
|
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
|
||||||
|
|
||||||
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
|
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
|
||||||
|
use askama_escape::{escape as escape_html_entity, Html};
|
||||||
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
||||||
use v_htmlescape::escape as escape_html_entity;
|
|
||||||
|
|
||||||
/// A directory; responds with the generated directory listing.
|
/// A directory; responds with the generated directory listing.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -50,7 +50,7 @@ macro_rules! encode_file_url {
|
|||||||
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
||||||
macro_rules! encode_file_name {
|
macro_rules! encode_file_name {
|
||||||
($entry:ident) => {
|
($entry:ident) => {
|
||||||
escape_html_entity(&$entry.file_name().to_string_lossy())
|
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,9 +66,7 @@ pub(crate) fn directory_listing(
|
|||||||
if dir.is_visible(&entry) {
|
if dir.is_visible(&entry) {
|
||||||
let entry = entry.unwrap();
|
let entry = entry.unwrap();
|
||||||
let p = match entry.path().strip_prefix(&dir.path) {
|
let p = match entry.path().strip_prefix(&dir.path) {
|
||||||
Ok(p) if cfg!(windows) => {
|
Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"),
|
||||||
base.join(p).to_string_lossy().replace("\\", "/")
|
|
||||||
}
|
|
||||||
Ok(p) => base.join(p).to_string_lossy().into_owned(),
|
Ok(p) => base.join(p).to_string_lossy().into_owned(),
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
use actix_web::{http::StatusCode, ResponseError};
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
|
|
||||||
/// Errors which can occur when serving static files.
|
/// Errors which can occur when serving static files.
|
||||||
@@ -16,11 +16,12 @@ pub enum FilesError {
|
|||||||
|
|
||||||
/// Return `NotFound` for `FilesError`
|
/// Return `NotFound` for `FilesError`
|
||||||
impl ResponseError for FilesError {
|
impl ResponseError for FilesError {
|
||||||
fn error_response(&self) -> HttpResponse {
|
fn status_code(&self) -> StatusCode {
|
||||||
HttpResponse::new(StatusCode::NOT_FOUND)
|
StatusCode::NOT_FOUND
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
#[derive(Display, Debug, PartialEq)]
|
#[derive(Display, Debug, PartialEq)]
|
||||||
pub enum UriSegmentError {
|
pub enum UriSegmentError {
|
||||||
/// The segment started with the wrapped invalid character.
|
/// The segment started with the wrapped invalid character.
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
fmt, io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{
|
dev::{
|
||||||
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
|
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
|
||||||
|
ServiceResponse,
|
||||||
},
|
},
|
||||||
error::Error,
|
error::Error,
|
||||||
guard::Guard,
|
guard::Guard,
|
||||||
http::header::DispositionType,
|
http::header::DispositionType,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
};
|
};
|
||||||
use futures_util::future::{ok, FutureExt, LocalBoxFuture};
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
directory_listing, named, Directory, DirectoryRenderer, FilesService,
|
directory_listing, named,
|
||||||
HttpNewService, MimeOverride,
|
service::{FilesService, FilesServiceInner},
|
||||||
|
Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Static files handling service.
|
/// Static files handling service.
|
||||||
///
|
///
|
||||||
/// `Files` service must be registered with `App::service()` method.
|
/// `Files` service must be registered with `App::service()` method.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```
|
||||||
/// use actix_web::App;
|
/// use actix_web::App;
|
||||||
/// use actix_files::Files;
|
/// use actix_files::Files;
|
||||||
///
|
///
|
||||||
@@ -37,8 +44,10 @@ pub struct Files {
|
|||||||
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
||||||
renderer: Rc<DirectoryRenderer>,
|
renderer: Rc<DirectoryRenderer>,
|
||||||
mime_override: Option<Rc<MimeOverride>>,
|
mime_override: Option<Rc<MimeOverride>>,
|
||||||
|
path_filter: Option<Rc<PathFilter>>,
|
||||||
file_flags: named::Flags,
|
file_flags: named::Flags,
|
||||||
guards: Option<Rc<dyn Guard>>,
|
use_guards: Option<Rc<dyn Guard>>,
|
||||||
|
guards: Vec<Rc<dyn Guard>>,
|
||||||
hidden_files: bool,
|
hidden_files: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +69,8 @@ impl Clone for Files {
|
|||||||
file_flags: self.file_flags,
|
file_flags: self.file_flags,
|
||||||
path: self.path.clone(),
|
path: self.path.clone(),
|
||||||
mime_override: self.mime_override.clone(),
|
mime_override: self.mime_override.clone(),
|
||||||
|
path_filter: self.path_filter.clone(),
|
||||||
|
use_guards: self.use_guards.clone(),
|
||||||
guards: self.guards.clone(),
|
guards: self.guards.clone(),
|
||||||
hidden_files: self.hidden_files,
|
hidden_files: self.hidden_files,
|
||||||
}
|
}
|
||||||
@@ -81,9 +92,9 @@ impl Files {
|
|||||||
/// If the mount path is set as the root path `/`, services registered after this one will
|
/// If the mount path is set as the root path `/`, services registered after this one will
|
||||||
/// be inaccessible. Register more specific handlers and services first.
|
/// be inaccessible. Register more specific handlers and services first.
|
||||||
///
|
///
|
||||||
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a
|
/// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
|
||||||
/// number of threads equal to 5x the number of available logical CPUs. Pool size can be changed
|
/// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
|
||||||
/// by setting ACTIX_THREADPOOL environment variable.
|
/// the number of server [workers](actix_web::HttpServer::workers), by default.
|
||||||
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
|
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
|
||||||
let orig_dir = serve_from.into();
|
let orig_dir = serve_from.into();
|
||||||
let dir = match orig_dir.canonicalize() {
|
let dir = match orig_dir.canonicalize() {
|
||||||
@@ -95,7 +106,7 @@ impl Files {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Files {
|
Files {
|
||||||
path: mount_path.to_owned(),
|
path: mount_path.trim_end_matches('/').to_owned(),
|
||||||
directory: dir,
|
directory: dir,
|
||||||
index: None,
|
index: None,
|
||||||
show_index: false,
|
show_index: false,
|
||||||
@@ -103,8 +114,10 @@ impl Files {
|
|||||||
default: Rc::new(RefCell::new(None)),
|
default: Rc::new(RefCell::new(None)),
|
||||||
renderer: Rc::new(directory_listing),
|
renderer: Rc::new(directory_listing),
|
||||||
mime_override: None,
|
mime_override: None,
|
||||||
|
path_filter: None,
|
||||||
file_flags: named::Flags::default(),
|
file_flags: named::Flags::default(),
|
||||||
guards: None,
|
use_guards: None,
|
||||||
|
guards: Vec::new(),
|
||||||
hidden_files: false,
|
hidden_files: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,6 +125,9 @@ impl Files {
|
|||||||
/// Show files listing for directories.
|
/// Show files listing for directories.
|
||||||
///
|
///
|
||||||
/// By default show files listing is disabled.
|
/// By default show files listing is disabled.
|
||||||
|
///
|
||||||
|
/// When used with [`Files::index_file()`], files listing is shown as a fallback
|
||||||
|
/// when the index file is not found.
|
||||||
pub fn show_files_listing(mut self) -> Self {
|
pub fn show_files_listing(mut self) -> Self {
|
||||||
self.show_index = true;
|
self.show_index = true;
|
||||||
self
|
self
|
||||||
@@ -128,8 +144,8 @@ impl Files {
|
|||||||
/// Set custom directory renderer
|
/// Set custom directory renderer
|
||||||
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
|
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
|
||||||
where
|
where
|
||||||
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
|
for<'r, 's> F:
|
||||||
+ 'static,
|
Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
|
||||||
{
|
{
|
||||||
self.renderer = Rc::new(f);
|
self.renderer = Rc::new(f);
|
||||||
self
|
self
|
||||||
@@ -144,10 +160,45 @@ impl Files {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets path filtering closure.
|
||||||
|
///
|
||||||
|
/// The path provided to the closure is relative to `serve_from` path.
|
||||||
|
/// You can safely join this path with the `serve_from` path to get the real path.
|
||||||
|
/// However, the real path may not exist since the filter is called before checking path existence.
|
||||||
|
///
|
||||||
|
/// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise,
|
||||||
|
/// `404 Not Found` is returned.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use std::path::Path;
|
||||||
|
/// use actix_files::Files;
|
||||||
|
///
|
||||||
|
/// // prevent searching subdirectories and following symlinks
|
||||||
|
/// let files_service = Files::new("/", "./static").path_filter(|path, _| {
|
||||||
|
/// path.components().count() == 1
|
||||||
|
/// && Path::new("./static")
|
||||||
|
/// .join(path)
|
||||||
|
/// .symlink_metadata()
|
||||||
|
/// .map(|m| !m.file_type().is_symlink())
|
||||||
|
/// .unwrap_or(false)
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub fn path_filter<F>(mut self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&Path, &RequestHead) -> bool + 'static,
|
||||||
|
{
|
||||||
|
self.path_filter = Some(Rc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set index file
|
/// Set index file
|
||||||
///
|
///
|
||||||
/// Shows specific index file for directory "/" instead of
|
/// Shows specific index file for directories instead of
|
||||||
/// showing files listing.
|
/// showing files listing.
|
||||||
|
///
|
||||||
|
/// If the index file is not found, files listing is shown as a fallback if
|
||||||
|
/// [`Files::show_files_listing()`] is set.
|
||||||
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
|
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
|
||||||
self.index = Some(index.into());
|
self.index = Some(index.into());
|
||||||
self
|
self
|
||||||
@@ -156,7 +207,6 @@ impl Files {
|
|||||||
/// Specifies whether to use ETag or not.
|
/// Specifies whether to use ETag or not.
|
||||||
///
|
///
|
||||||
/// Default is true.
|
/// Default is true.
|
||||||
#[inline]
|
|
||||||
pub fn use_etag(mut self, value: bool) -> Self {
|
pub fn use_etag(mut self, value: bool) -> Self {
|
||||||
self.file_flags.set(named::Flags::ETAG, value);
|
self.file_flags.set(named::Flags::ETAG, value);
|
||||||
self
|
self
|
||||||
@@ -165,7 +215,6 @@ impl Files {
|
|||||||
/// Specifies whether to use Last-Modified or not.
|
/// Specifies whether to use Last-Modified or not.
|
||||||
///
|
///
|
||||||
/// Default is true.
|
/// Default is true.
|
||||||
#[inline]
|
|
||||||
pub fn use_last_modified(mut self, value: bool) -> Self {
|
pub fn use_last_modified(mut self, value: bool) -> Self {
|
||||||
self.file_flags.set(named::Flags::LAST_MD, value);
|
self.file_flags.set(named::Flags::LAST_MD, value);
|
||||||
self
|
self
|
||||||
@@ -174,31 +223,80 @@ impl Files {
|
|||||||
/// Specifies whether text responses should signal a UTF-8 encoding.
|
/// Specifies whether text responses should signal a UTF-8 encoding.
|
||||||
///
|
///
|
||||||
/// Default is false (but will default to true in a future version).
|
/// Default is false (but will default to true in a future version).
|
||||||
#[inline]
|
|
||||||
pub fn prefer_utf8(mut self, value: bool) -> Self {
|
pub fn prefer_utf8(mut self, value: bool) -> Self {
|
||||||
self.file_flags.set(named::Flags::PREFER_UTF8, value);
|
self.file_flags.set(named::Flags::PREFER_UTF8, value);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specifies custom guards to use for directory listings and files.
|
/// Adds a routing guard.
|
||||||
///
|
///
|
||||||
/// Default behaviour allows GET and HEAD.
|
/// Use this to allow multiple chained file services that respond to strictly different
|
||||||
#[inline]
|
/// properties of a request. Due to the way routing works, if a guard check returns true and the
|
||||||
pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self {
|
/// request starts being handled by the file service, it will not be able to back-out and try
|
||||||
self.guards = Some(Rc::new(guards));
|
/// the next service, you will simply get a 404 (or 405) error response.
|
||||||
|
///
|
||||||
|
/// To allow `POST` requests to retrieve files, see [`Files::use_guards`].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::{guard::Header, App};
|
||||||
|
/// use actix_files::Files;
|
||||||
|
///
|
||||||
|
/// App::new().service(
|
||||||
|
/// Files::new("/","/my/site/files")
|
||||||
|
/// .guard(Header("Host", "example.com"))
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
|
||||||
|
self.guards.push(Rc::new(guard));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specifies guard to check before fetching directory listings or files.
|
||||||
|
///
|
||||||
|
/// Note that this guard has no effect on routing; it's main use is to guard on the request's
|
||||||
|
/// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
|
||||||
|
/// See [`Files::guard`] for routing guards.
|
||||||
|
pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
|
||||||
|
self.use_guards = Some(Rc::new(guard));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
|
||||||
|
/// See [`Files::method_guard`].
|
||||||
|
pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
|
||||||
|
self.method_guard(guard)
|
||||||
|
}
|
||||||
|
|
||||||
/// Disable `Content-Disposition` header.
|
/// Disable `Content-Disposition` header.
|
||||||
///
|
///
|
||||||
/// By default Content-Disposition` header is enabled.
|
/// By default Content-Disposition` header is enabled.
|
||||||
#[inline]
|
|
||||||
pub fn disable_content_disposition(mut self) -> Self {
|
pub fn disable_content_disposition(mut self) -> Self {
|
||||||
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
|
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets default handler which is used when no matched file could be found.
|
/// Sets default handler which is used when no matched file could be found.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// Setting a fallback static file handler:
|
||||||
|
/// ```
|
||||||
|
/// use actix_files::{Files, NamedFile};
|
||||||
|
/// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
|
||||||
|
///
|
||||||
|
/// # fn run() -> Result<(), actix_web::Error> {
|
||||||
|
/// let files = Files::new("/", "./static")
|
||||||
|
/// .index_file("index.html")
|
||||||
|
/// .default_handler(fn_service(|req: ServiceRequest| async {
|
||||||
|
/// let (req, _) = req.into_parts();
|
||||||
|
/// let file = NamedFile::open_async("./static/404.html").await?;
|
||||||
|
/// let res = file.into_response(&req);
|
||||||
|
/// Ok(ServiceResponse::new(req, res))
|
||||||
|
/// }));
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
pub fn default_handler<F, U>(mut self, f: F) -> Self
|
pub fn default_handler<F, U>(mut self, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: IntoServiceFactory<U, ServiceRequest>,
|
F: IntoServiceFactory<U, ServiceRequest>,
|
||||||
@@ -218,7 +316,6 @@ impl Files {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Enables serving hidden files and directories, allowing a leading dots in url fragments.
|
/// Enables serving hidden files and directories, allowing a leading dots in url fragments.
|
||||||
#[inline]
|
|
||||||
pub fn use_hidden_files(mut self) -> Self {
|
pub fn use_hidden_files(mut self) -> Self {
|
||||||
self.hidden_files = true;
|
self.hidden_files = true;
|
||||||
self
|
self
|
||||||
@@ -226,7 +323,19 @@ impl Files {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HttpServiceFactory for Files {
|
impl HttpServiceFactory for Files {
|
||||||
fn register(self, config: &mut AppService) {
|
fn register(mut self, config: &mut AppService) {
|
||||||
|
let guards = if self.guards.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let guards = std::mem::take(&mut self.guards);
|
||||||
|
Some(
|
||||||
|
guards
|
||||||
|
.into_iter()
|
||||||
|
.map(|guard| -> Box<dyn Guard> { Box::new(guard) })
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
if self.default.borrow().is_none() {
|
if self.default.borrow().is_none() {
|
||||||
*self.default.borrow_mut() = Some(config.default_service());
|
*self.default.borrow_mut() = Some(config.default_service());
|
||||||
}
|
}
|
||||||
@@ -237,7 +346,7 @@ impl HttpServiceFactory for Files {
|
|||||||
ResourceDef::prefix(&self.path)
|
ResourceDef::prefix(&self.path)
|
||||||
};
|
};
|
||||||
|
|
||||||
config.register_service(rdef, None, self, None)
|
config.register_service(rdef, guards, self, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +359,7 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||||
|
|
||||||
fn new_service(&self, _: ()) -> Self::Future {
|
fn new_service(&self, _: ()) -> Self::Future {
|
||||||
let mut srv = FilesService {
|
let mut inner = FilesServiceInner {
|
||||||
directory: self.directory.clone(),
|
directory: self.directory.clone(),
|
||||||
index: self.index.clone(),
|
index: self.index.clone(),
|
||||||
show_index: self.show_index,
|
show_index: self.show_index,
|
||||||
@@ -258,24 +367,25 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||||||
default: None,
|
default: None,
|
||||||
renderer: self.renderer.clone(),
|
renderer: self.renderer.clone(),
|
||||||
mime_override: self.mime_override.clone(),
|
mime_override: self.mime_override.clone(),
|
||||||
|
path_filter: self.path_filter.clone(),
|
||||||
file_flags: self.file_flags,
|
file_flags: self.file_flags,
|
||||||
guards: self.guards.clone(),
|
guards: self.use_guards.clone(),
|
||||||
hidden_files: self.hidden_files,
|
hidden_files: self.hidden_files,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref default) = *self.default.borrow() {
|
if let Some(ref default) = *self.default.borrow() {
|
||||||
default
|
let fut = default.new_service(());
|
||||||
.new_service(())
|
Box::pin(async {
|
||||||
.map(move |result| match result {
|
match fut.await {
|
||||||
Ok(default) => {
|
Ok(default) => {
|
||||||
srv.default = Some(default);
|
inner.default = Some(default);
|
||||||
Ok(srv)
|
Ok(FilesService(Rc::new(inner)))
|
||||||
}
|
}
|
||||||
Err(_) => Err(()),
|
Err(_) => Err(()),
|
||||||
})
|
}
|
||||||
.boxed_local()
|
})
|
||||||
} else {
|
} else {
|
||||||
ok(srv).boxed_local()
|
Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Provides a non-blocking service for serving static files from disk.
|
//! Provides a non-blocking service for serving static files from disk.
|
||||||
//!
|
//!
|
||||||
//! # Example
|
//! # Example
|
||||||
//! ```rust
|
//! ```
|
||||||
//! use actix_web::App;
|
//! use actix_web::App;
|
||||||
//! use actix_files::Files;
|
//! use actix_files::Files;
|
||||||
//!
|
//!
|
||||||
@@ -14,15 +14,14 @@
|
|||||||
#![deny(rust_2018_idioms)]
|
#![deny(rust_2018_idioms)]
|
||||||
#![warn(missing_docs, missing_debug_implementations)]
|
#![warn(missing_docs, missing_debug_implementations)]
|
||||||
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{ServiceRequest, ServiceResponse},
|
dev::{RequestHead, ServiceRequest, ServiceResponse},
|
||||||
error::{BlockingError, Error, ErrorInternalServerError},
|
error::Error,
|
||||||
http::header::DispositionType,
|
http::header::DispositionType,
|
||||||
};
|
};
|
||||||
use mime_guess::from_ext;
|
use mime_guess::from_ext;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
mod chunked;
|
mod chunked;
|
||||||
mod directory;
|
mod directory;
|
||||||
@@ -34,12 +33,12 @@ mod path_buf;
|
|||||||
mod range;
|
mod range;
|
||||||
mod service;
|
mod service;
|
||||||
|
|
||||||
pub use crate::chunked::ChunkedReadFile;
|
pub use self::chunked::ChunkedReadFile;
|
||||||
pub use crate::directory::Directory;
|
pub use self::directory::Directory;
|
||||||
pub use crate::files::Files;
|
pub use self::files::Files;
|
||||||
pub use crate::named::NamedFile;
|
pub use self::named::NamedFile;
|
||||||
pub use crate::range::HttpRange;
|
pub use self::range::HttpRange;
|
||||||
pub use crate::service::FilesService;
|
pub use self::service::FilesService;
|
||||||
|
|
||||||
use self::directory::{directory_listing, DirectoryRenderer};
|
use self::directory::{directory_listing, DirectoryRenderer};
|
||||||
use self::error::FilesError;
|
use self::error::FilesError;
|
||||||
@@ -56,19 +55,14 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
|
|||||||
from_ext(ext).first_or_octet_stream()
|
from_ext(ext).first_or_octet_stream()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_error(err: BlockingError<io::Error>) -> Error {
|
|
||||||
match err {
|
|
||||||
BlockingError::Error(err) => err.into(),
|
|
||||||
BlockingError::Canceled => ErrorInternalServerError("Unexpected error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
|
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
|
||||||
|
|
||||||
|
type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
fs::{self},
|
||||||
ops::Add,
|
ops::Add,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
@@ -82,13 +76,14 @@ mod tests {
|
|||||||
},
|
},
|
||||||
middleware::Compress,
|
middleware::Compress,
|
||||||
test::{self, TestRequest},
|
test::{self, TestRequest},
|
||||||
web, App, HttpResponse, Responder,
|
web::{self, Bytes},
|
||||||
|
App, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
use futures_util::future::ok;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::named::File;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_web::test]
|
||||||
async fn test_file_extension_to_mime() {
|
async fn test_file_extension_to_mime() {
|
||||||
let m = file_extension_to_mime("");
|
let m = file_extension_to_mime("");
|
||||||
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
|
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
|
||||||
@@ -105,12 +100,23 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_if_modified_since_without_if_none_match() {
|
async fn test_if_modified_since_without_if_none_match() {
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let since =
|
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
||||||
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
|
||||||
|
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.header(header::IF_MODIFIED_SINCE, since)
|
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||||
|
.to_http_request();
|
||||||
|
let resp = file.respond_to(&req).await.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_if_modified_since_without_if_none_match_same() {
|
||||||
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
|
let since = file.last_modified().unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::default()
|
||||||
|
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req).await.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||||
@@ -118,22 +124,45 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_if_modified_since_with_if_none_match() {
|
async fn test_if_modified_since_with_if_none_match() {
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let since =
|
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
||||||
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
|
||||||
|
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.header(header::IF_NONE_MATCH, "miss_etag")
|
.insert_header((header::IF_NONE_MATCH, "miss_etag"))
|
||||||
.header(header::IF_MODIFIED_SINCE, since)
|
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req).await.unwrap();
|
||||||
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
|
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_if_unmodified_since() {
|
||||||
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
|
let since = file.last_modified().unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::default()
|
||||||
|
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||||
|
.to_http_request();
|
||||||
|
let resp = file.respond_to(&req).await.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_if_unmodified_since_failed() {
|
||||||
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
|
let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
|
||||||
|
|
||||||
|
let req = TestRequest::default()
|
||||||
|
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||||
|
.to_http_request();
|
||||||
|
let resp = file.respond_to(&req).await.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_text() {
|
async fn test_named_file_text() {
|
||||||
assert!(NamedFile::open("test--").is_err());
|
assert!(NamedFile::open_async("test--").await.is_err());
|
||||||
let mut file = NamedFile::open("Cargo.toml").unwrap();
|
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -156,8 +185,8 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_content_disposition() {
|
async fn test_named_file_content_disposition() {
|
||||||
assert!(NamedFile::open("test--").is_err());
|
assert!(NamedFile::open_async("test--").await.is_err());
|
||||||
let mut file = NamedFile::open("Cargo.toml").unwrap();
|
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -173,7 +202,8 @@ mod tests {
|
|||||||
"inline; filename=\"Cargo.toml\""
|
"inline; filename=\"Cargo.toml\""
|
||||||
);
|
);
|
||||||
|
|
||||||
let file = NamedFile::open("Cargo.toml")
|
let file = NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.disable_content_disposition();
|
.disable_content_disposition();
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
@@ -183,9 +213,19 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_non_ascii_file_name() {
|
async fn test_named_file_non_ascii_file_name() {
|
||||||
let mut file =
|
let file = {
|
||||||
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml")
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
.unwrap();
|
{
|
||||||
|
crate::named::File::open("Cargo.toml").await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
{
|
||||||
|
crate::named::File::open("Cargo.toml").unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file = NamedFile::from_file(file, "貨物.toml").unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -208,7 +248,8 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_set_content_type() {
|
async fn test_named_file_set_content_type() {
|
||||||
let mut file = NamedFile::open("Cargo.toml")
|
let mut file = NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_content_type(mime::TEXT_XML);
|
.set_content_type(mime::TEXT_XML);
|
||||||
{
|
{
|
||||||
@@ -233,7 +274,7 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_image() {
|
async fn test_named_file_image() {
|
||||||
let mut file = NamedFile::open("tests/test.png").unwrap();
|
let mut file = NamedFile::open_async("tests/test.png").await.unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -254,13 +295,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_named_file_javascript() {
|
||||||
|
let file = NamedFile::open_async("tests/test.js").await.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let resp = file.respond_to(&req).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
|
"application/javascript"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
|
"inline; filename=\"test.js\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_image_attachment() {
|
async fn test_named_file_image_attachment() {
|
||||||
let cd = ContentDisposition {
|
let cd = ContentDisposition {
|
||||||
disposition: DispositionType::Attachment,
|
disposition: DispositionType::Attachment,
|
||||||
parameters: vec![DispositionParam::Filename(String::from("test.png"))],
|
parameters: vec![DispositionParam::Filename(String::from("test.png"))],
|
||||||
};
|
};
|
||||||
let mut file = NamedFile::open("tests/test.png")
|
let mut file = NamedFile::open_async("tests/test.png")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_content_disposition(cd);
|
.set_content_disposition(cd);
|
||||||
{
|
{
|
||||||
@@ -285,7 +343,7 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_binary() {
|
async fn test_named_file_binary() {
|
||||||
let mut file = NamedFile::open("tests/test.binary").unwrap();
|
let mut file = NamedFile::open_async("tests/test.binary").await.unwrap();
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
@@ -308,7 +366,8 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_status_code_text() {
|
async fn test_named_file_status_code_text() {
|
||||||
let mut file = NamedFile::open("Cargo.toml")
|
let mut file = NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_status_code(StatusCode::NOT_FOUND);
|
.set_status_code(StatusCode::NOT_FOUND);
|
||||||
{
|
{
|
||||||
@@ -338,7 +397,7 @@ mod tests {
|
|||||||
DispositionType::Attachment
|
DispositionType::Attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut srv = test::init_service(
|
let srv = test::init_service(
|
||||||
App::new().service(
|
App::new().service(
|
||||||
Files::new("/", ".")
|
Files::new("/", ".")
|
||||||
.mime_override(all_attachment)
|
.mime_override(all_attachment)
|
||||||
@@ -348,7 +407,7 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let request = TestRequest::get().uri("/").to_request();
|
let request = TestRequest::get().uri("/").to_request();
|
||||||
let response = test::call_service(&mut srv, request).await;
|
let response = test::call_service(&srv, request).await;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
let content_disposition = response
|
let content_disposition = response
|
||||||
@@ -363,7 +422,7 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_ranges_status_code() {
|
async fn test_named_file_ranges_status_code() {
|
||||||
let mut srv = test::init_service(
|
let srv = test::init_service(
|
||||||
App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
|
App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -371,29 +430,29 @@ mod tests {
|
|||||||
// Valid range header
|
// Valid range header
|
||||||
let request = TestRequest::get()
|
let request = TestRequest::get()
|
||||||
.uri("/t%65st/Cargo.toml")
|
.uri("/t%65st/Cargo.toml")
|
||||||
.header(header::RANGE, "bytes=10-20")
|
.insert_header((header::RANGE, "bytes=10-20"))
|
||||||
.to_request();
|
.to_request();
|
||||||
let response = test::call_service(&mut srv, request).await;
|
let response = test::call_service(&srv, request).await;
|
||||||
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
|
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
|
||||||
|
|
||||||
// Invalid range header
|
// Invalid range header
|
||||||
let request = TestRequest::get()
|
let request = TestRequest::get()
|
||||||
.uri("/t%65st/Cargo.toml")
|
.uri("/t%65st/Cargo.toml")
|
||||||
.header(header::RANGE, "bytes=1-0")
|
.insert_header((header::RANGE, "bytes=1-0"))
|
||||||
.to_request();
|
.to_request();
|
||||||
let response = test::call_service(&mut srv, request).await;
|
let response = test::call_service(&srv, request).await;
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
|
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_content_range_headers() {
|
async fn test_named_file_content_range_headers() {
|
||||||
let srv = test::start(|| App::new().service(Files::new("/", ".")));
|
let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
|
||||||
|
|
||||||
// Valid range header
|
// Valid range header
|
||||||
let response = srv
|
let response = srv
|
||||||
.get("/tests/test.binary")
|
.get("/tests/test.binary")
|
||||||
.header(header::RANGE, "bytes=10-20")
|
.insert_header((header::RANGE, "bytes=10-20"))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -403,7 +462,7 @@ mod tests {
|
|||||||
// Invalid range header
|
// Invalid range header
|
||||||
let response = srv
|
let response = srv
|
||||||
.get("/tests/test.binary")
|
.get("/tests/test.binary")
|
||||||
.header(header::RANGE, "bytes=10-5")
|
.insert_header((header::RANGE, "bytes=10-5"))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -413,12 +472,12 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_content_length_headers() {
|
async fn test_named_file_content_length_headers() {
|
||||||
let srv = test::start(|| App::new().service(Files::new("/", ".")));
|
let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
|
||||||
|
|
||||||
// Valid range header
|
// Valid range header
|
||||||
let response = srv
|
let response = srv
|
||||||
.get("/tests/test.binary")
|
.get("/tests/test.binary")
|
||||||
.header(header::RANGE, "bytes=10-20")
|
.insert_header((header::RANGE, "bytes=10-20"))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -428,7 +487,7 @@ mod tests {
|
|||||||
// Valid range header, starting from 0
|
// Valid range header, starting from 0
|
||||||
let response = srv
|
let response = srv
|
||||||
.get("/tests/test.binary")
|
.get("/tests/test.binary")
|
||||||
.header(header::RANGE, "bytes=0-20")
|
.insert_header((header::RANGE, "bytes=0-20"))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -452,7 +511,7 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_head_content_length_headers() {
|
async fn test_head_content_length_headers() {
|
||||||
let srv = test::start(|| App::new().service(Files::new("/", ".")));
|
let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
|
||||||
|
|
||||||
let response = srv.head("/tests/test.binary").send().await.unwrap();
|
let response = srv.head("/tests/test.binary").send().await.unwrap();
|
||||||
|
|
||||||
@@ -468,14 +527,14 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_static_files_with_spaces() {
|
async fn test_static_files_with_spaces() {
|
||||||
let mut srv = test::init_service(
|
let srv = test::init_service(
|
||||||
App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
|
App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let request = TestRequest::get()
|
let request = TestRequest::get()
|
||||||
.uri("/tests/test%20space.binary")
|
.uri("/tests/test%20space.binary")
|
||||||
.to_request();
|
.to_request();
|
||||||
let response = test::call_service(&mut srv, request).await;
|
let response = test::call_service(&srv, request).await;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
let bytes = test::read_body(response).await;
|
let bytes = test::read_body(response).await;
|
||||||
@@ -485,29 +544,29 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_files_not_allowed() {
|
async fn test_files_not_allowed() {
|
||||||
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
||||||
|
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.uri("/Cargo.toml")
|
.uri("/Cargo.toml")
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.to_request();
|
.to_request();
|
||||||
|
|
||||||
let resp = test::call_service(&mut srv, req).await;
|
let resp = test::call_service(&srv, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||||
|
|
||||||
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.method(Method::PUT)
|
.method(Method::PUT)
|
||||||
.uri("/Cargo.toml")
|
.uri("/Cargo.toml")
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = test::call_service(&mut srv, req).await;
|
let resp = test::call_service(&srv, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_files_guards() {
|
async fn test_files_guards() {
|
||||||
let mut srv = test::init_service(
|
let srv = test::init_service(
|
||||||
App::new().service(Files::new("/", ".").use_guards(guard::Post())),
|
App::new().service(Files::new("/", ".").method_guard(guard::Post())),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -516,15 +575,16 @@ mod tests {
|
|||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.to_request();
|
.to_request();
|
||||||
|
|
||||||
let resp = test::call_service(&mut srv, req).await;
|
let resp = test::call_service(&srv, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_content_encoding() {
|
async fn test_named_file_content_encoding() {
|
||||||
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
|
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||||
web::resource("/").to(|| async {
|
web::resource("/").to(|| async {
|
||||||
NamedFile::open("Cargo.toml")
|
NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_content_encoding(header::ContentEncoding::Identity)
|
.set_content_encoding(header::ContentEncoding::Identity)
|
||||||
}),
|
}),
|
||||||
@@ -533,18 +593,19 @@ mod tests {
|
|||||||
|
|
||||||
let request = TestRequest::get()
|
let request = TestRequest::get()
|
||||||
.uri("/")
|
.uri("/")
|
||||||
.header(header::ACCEPT_ENCODING, "gzip")
|
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||||
.to_request();
|
.to_request();
|
||||||
let res = test::call_service(&mut srv, request).await;
|
let res = test::call_service(&srv, request).await;
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
|
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_content_encoding_gzip() {
|
async fn test_named_file_content_encoding_gzip() {
|
||||||
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
|
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||||
web::resource("/").to(|| async {
|
web::resource("/").to(|| async {
|
||||||
NamedFile::open("Cargo.toml")
|
NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_content_encoding(header::ContentEncoding::Gzip)
|
.set_content_encoding(header::ContentEncoding::Gzip)
|
||||||
}),
|
}),
|
||||||
@@ -553,9 +614,9 @@ mod tests {
|
|||||||
|
|
||||||
let request = TestRequest::get()
|
let request = TestRequest::get()
|
||||||
.uri("/")
|
.uri("/")
|
||||||
.header(header::ACCEPT_ENCODING, "gzip")
|
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||||
.to_request();
|
.to_request();
|
||||||
let res = test::call_service(&mut srv, request).await;
|
let res = test::call_service(&srv, request).await;
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
res.headers()
|
res.headers()
|
||||||
@@ -570,34 +631,32 @@ mod tests {
|
|||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_allowed_method() {
|
async fn test_named_file_allowed_method() {
|
||||||
let req = TestRequest::default().method(Method::GET).to_http_request();
|
let req = TestRequest::default().method(Method::GET).to_http_request();
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
let resp = file.respond_to(&req).await.unwrap();
|
let resp = file.respond_to(&req).await.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_static_files() {
|
async fn test_static_files() {
|
||||||
let mut srv = test::init_service(
|
let srv =
|
||||||
App::new().service(Files::new("/", ".").show_files_listing()),
|
test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
|
||||||
)
|
.await;
|
||||||
.await;
|
|
||||||
let req = TestRequest::with_uri("/missing").to_request();
|
let req = TestRequest::with_uri("/missing").to_request();
|
||||||
|
|
||||||
let resp = test::call_service(&mut srv, req).await;
|
let resp = test::call_service(&srv, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
||||||
|
|
||||||
let req = TestRequest::default().to_request();
|
let req = TestRequest::default().to_request();
|
||||||
let resp = test::call_service(&mut srv, req).await;
|
let resp = test::call_service(&srv, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let mut srv = test::init_service(
|
let srv =
|
||||||
App::new().service(Files::new("/", ".").show_files_listing()),
|
test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
|
||||||
)
|
.await;
|
||||||
.await;
|
|
||||||
let req = TestRequest::with_uri("/tests").to_request();
|
let req = TestRequest::with_uri("/tests").to_request();
|
||||||
let resp = test::call_service(&mut srv, req).await;
|
let resp = test::call_service(&srv, req).await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
"text/html; charset=utf-8"
|
"text/html; charset=utf-8"
|
||||||
@@ -609,17 +668,17 @@ mod tests {
|
|||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_redirect_to_slash_directory() {
|
async fn test_redirect_to_slash_directory() {
|
||||||
// should not redirect if no index
|
// should not redirect if no index and files listing is disabled
|
||||||
let mut srv = test::init_service(
|
let srv = test::init_service(
|
||||||
App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
|
App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let req = TestRequest::with_uri("/tests").to_request();
|
let req = TestRequest::with_uri("/tests").to_request();
|
||||||
let resp = test::call_service(&mut srv, req).await;
|
let resp = test::call_service(&srv, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
// should redirect if index present
|
// should redirect if index present
|
||||||
let mut srv = test::init_service(
|
let srv = test::init_service(
|
||||||
App::new().service(
|
App::new().service(
|
||||||
Files::new("/", ".")
|
Files::new("/", ".")
|
||||||
.index_file("test.png")
|
.index_file("test.png")
|
||||||
@@ -628,149 +687,270 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let req = TestRequest::with_uri("/tests").to_request();
|
let req = TestRequest::with_uri("/tests").to_request();
|
||||||
let resp = test::call_service(&mut srv, req).await;
|
let resp = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||||
|
|
||||||
|
// should redirect if files listing is enabled
|
||||||
|
let srv = test::init_service(
|
||||||
|
App::new().service(
|
||||||
|
Files::new("/", ".")
|
||||||
|
.show_files_listing()
|
||||||
|
.redirect_to_slash_directory(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let req = TestRequest::with_uri("/tests").to_request();
|
||||||
|
let resp = test::call_service(&srv, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||||
|
|
||||||
// should not redirect if the path is wrong
|
// should not redirect if the path is wrong
|
||||||
let req = TestRequest::with_uri("/not_existing").to_request();
|
let req = TestRequest::with_uri("/not_existing").to_request();
|
||||||
let resp = test::call_service(&mut srv, req).await;
|
let resp = test::call_service(&srv, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_static_files_bad_directory() {
|
async fn test_static_files_bad_directory() {
|
||||||
let _st: Files = Files::new("/", "missing");
|
let service = Files::new("/", "./missing").new_service(()).await.unwrap();
|
||||||
let _st: Files = Files::new("/", "Cargo.toml");
|
|
||||||
|
let req = TestRequest::with_uri("/").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
|
||||||
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_default_handler_file_missing() {
|
async fn test_default_handler_file_missing() {
|
||||||
let mut st = Files::new("/", ".")
|
let st = Files::new("/", ".")
|
||||||
.default_handler(|req: ServiceRequest| {
|
.default_handler(|req: ServiceRequest| async {
|
||||||
ok(req.into_response(HttpResponse::Ok().body("default content")))
|
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||||
})
|
})
|
||||||
.new_service(())
|
.new_service(())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let req = TestRequest::with_uri("/missing").to_srv_request();
|
let req = TestRequest::with_uri("/missing").to_srv_request();
|
||||||
|
let resp = test::call_service(&st, req).await;
|
||||||
|
|
||||||
let resp = test::call_service(&mut st, req).await;
|
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
let bytes = test::read_body(resp).await;
|
let bytes = test::read_body(resp).await;
|
||||||
assert_eq!(bytes, web::Bytes::from_static(b"default content"));
|
assert_eq!(bytes, web::Bytes::from_static(b"default content"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[actix_rt::test]
|
#[actix_rt::test]
|
||||||
// async fn test_serve_index() {
|
async fn test_serve_index_nested() {
|
||||||
// let st = Files::new(".").index_file("test.binary");
|
let service = Files::new(".", ".")
|
||||||
// let req = TestRequest::default().uri("/tests").finish();
|
.index_file("lib.rs")
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
let req = TestRequest::default().uri("/src").to_srv_request();
|
||||||
// let resp = resp.as_msg();
|
let resp = test::call_service(&service, req).await;
|
||||||
// assert_eq!(resp.status(), StatusCode::OK);
|
|
||||||
// assert_eq!(
|
|
||||||
// resp.headers()
|
|
||||||
// .get(header::CONTENT_TYPE)
|
|
||||||
// .expect("content type"),
|
|
||||||
// "application/octet-stream"
|
|
||||||
// );
|
|
||||||
// assert_eq!(
|
|
||||||
// resp.headers()
|
|
||||||
// .get(header::CONTENT_DISPOSITION)
|
|
||||||
// .expect("content disposition"),
|
|
||||||
// "attachment; filename=\"test.binary\""
|
|
||||||
// );
|
|
||||||
|
|
||||||
// let req = TestRequest::default().uri("/tests/").finish();
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
assert_eq!(
|
||||||
// let resp = resp.as_msg();
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
// assert_eq!(resp.status(), StatusCode::OK);
|
"text/x-rust"
|
||||||
// assert_eq!(
|
);
|
||||||
// resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
assert_eq!(
|
||||||
// "application/octet-stream"
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
// );
|
"inline; filename=\"lib.rs\""
|
||||||
// assert_eq!(
|
);
|
||||||
// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
}
|
||||||
// "attachment; filename=\"test.binary\""
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // nonexistent index file
|
#[actix_rt::test]
|
||||||
// let req = TestRequest::default().uri("/tests/unknown").finish();
|
async fn integration_serve_index() {
|
||||||
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
let srv = test::init_service(
|
||||||
// let resp = resp.as_msg();
|
App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
|
||||||
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
// let req = TestRequest::default().uri("/tests/unknown/").finish();
|
let req = TestRequest::get().uri("/test").to_request();
|
||||||
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
let res = test::call_service(&srv, req).await;
|
||||||
// let resp = resp.as_msg();
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[actix_rt::test]
|
let bytes = test::read_body(res).await;
|
||||||
// async fn test_serve_index_nested() {
|
|
||||||
// let st = Files::new(".").index_file("mod.rs");
|
|
||||||
// let req = TestRequest::default().uri("/src/client").finish();
|
|
||||||
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
|
||||||
// let resp = resp.as_msg();
|
|
||||||
// assert_eq!(resp.status(), StatusCode::OK);
|
|
||||||
// assert_eq!(
|
|
||||||
// resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
|
||||||
// "text/x-rust"
|
|
||||||
// );
|
|
||||||
// assert_eq!(
|
|
||||||
// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
|
||||||
// "inline; filename=\"mod.rs\""
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[actix_rt::test]
|
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||||
// fn integration_serve_index() {
|
assert_eq!(bytes, data);
|
||||||
// let mut srv = test::TestServer::with_factory(|| {
|
|
||||||
// App::new().handler(
|
|
||||||
// "test",
|
|
||||||
// Files::new(".").index_file("Cargo.toml"),
|
|
||||||
// )
|
|
||||||
// });
|
|
||||||
|
|
||||||
// let request = srv.get().uri(srv.url("/test")).finish().unwrap();
|
let req = TestRequest::get().uri("/test/").to_request();
|
||||||
// let response = srv.execute(request.send()).unwrap();
|
let res = test::call_service(&srv, req).await;
|
||||||
// assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
// let bytes = srv.execute(response.body()).unwrap();
|
|
||||||
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
|
||||||
// assert_eq!(bytes, data);
|
|
||||||
|
|
||||||
// let request = srv.get().uri(srv.url("/test/")).finish().unwrap();
|
let bytes = test::read_body(res).await;
|
||||||
// let response = srv.execute(request.send()).unwrap();
|
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||||
// assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(bytes, data);
|
||||||
// let bytes = srv.execute(response.body()).unwrap();
|
|
||||||
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
|
||||||
// assert_eq!(bytes, data);
|
|
||||||
|
|
||||||
// // nonexistent index file
|
// nonexistent index file
|
||||||
// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap();
|
let req = TestRequest::get().uri("/test/unknown").to_request();
|
||||||
// let response = srv.execute(request.send()).unwrap();
|
let res = test::call_service(&srv, req).await;
|
||||||
// assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap();
|
let req = TestRequest::get().uri("/test/unknown/").to_request();
|
||||||
// let response = srv.execute(request.send()).unwrap();
|
let res = test::call_service(&srv, req).await;
|
||||||
// assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// #[actix_rt::test]
|
#[actix_rt::test]
|
||||||
// fn integration_percent_encoded() {
|
async fn integration_percent_encoded() {
|
||||||
// let mut srv = test::TestServer::with_factory(|| {
|
let srv = test::init_service(
|
||||||
// App::new().handler(
|
App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
|
||||||
// "test",
|
)
|
||||||
// Files::new(".").index_file("Cargo.toml"),
|
.await;
|
||||||
// )
|
|
||||||
// });
|
|
||||||
|
|
||||||
// let request = srv
|
let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
|
||||||
// .get()
|
let res = test::call_service(&srv, req).await;
|
||||||
// .uri(srv.url("/test/%43argo.toml"))
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
// .finish()
|
}
|
||||||
// .unwrap();
|
|
||||||
// let response = srv.execute(request.send()).unwrap();
|
#[actix_rt::test]
|
||||||
// assert_eq!(response.status(), StatusCode::OK);
|
async fn test_serve_named_file() {
|
||||||
// }
|
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
|
let srv = test::init_service(App::new().service(factory)).await;
|
||||||
|
|
||||||
|
let req = TestRequest::get().uri("/Cargo.toml").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let bytes = test::read_body(res).await;
|
||||||
|
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||||
|
assert_eq!(bytes, data);
|
||||||
|
|
||||||
|
let req = TestRequest::get().uri("/test/unknown").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_serve_named_file_prefix() {
|
||||||
|
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
|
let srv =
|
||||||
|
test::init_service(App::new().service(web::scope("/test").service(factory))).await;
|
||||||
|
|
||||||
|
let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let bytes = test::read_body(res).await;
|
||||||
|
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||||
|
assert_eq!(bytes, data);
|
||||||
|
|
||||||
|
let req = TestRequest::get().uri("/Cargo.toml").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_named_file_default_service() {
|
||||||
|
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
|
let srv = test::init_service(App::new().default_service(factory)).await;
|
||||||
|
|
||||||
|
for route in ["/foobar", "/baz", "/"].iter() {
|
||||||
|
let req = TestRequest::get().uri(route).to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let bytes = test::read_body(res).await;
|
||||||
|
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||||
|
assert_eq!(bytes, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_default_handler_named_file() {
|
||||||
|
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
|
let st = Files::new("/", ".")
|
||||||
|
.default_handler(factory)
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let req = TestRequest::with_uri("/missing").to_srv_request();
|
||||||
|
let resp = test::call_service(&st, req).await;
|
||||||
|
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
let bytes = test::read_body(resp).await;
|
||||||
|
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||||
|
assert_eq!(bytes, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_symlinks() {
|
||||||
|
let srv = test::init_service(App::new().service(Files::new("test", "."))).await;
|
||||||
|
|
||||||
|
let req = TestRequest::get()
|
||||||
|
.uri("/test/tests/symlink-test.png")
|
||||||
|
.to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
res.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
|
"inline; filename=\"symlink-test.png\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_index_with_show_files_listing() {
|
||||||
|
let service = Files::new(".", ".")
|
||||||
|
.index_file("lib.rs")
|
||||||
|
.show_files_listing()
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Serve the index if exists
|
||||||
|
let req = TestRequest::default().uri("/src").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
|
"text/x-rust"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show files listing, otherwise.
|
||||||
|
let req = TestRequest::default().uri("/tests").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
|
"text/html; charset=utf-8"
|
||||||
|
);
|
||||||
|
let bytes = test::read_body(resp).await;
|
||||||
|
assert!(format!("{:?}", bytes).contains("/tests/test.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_path_filter() {
|
||||||
|
// prevent searching subdirectories
|
||||||
|
let st = Files::new("/", ".")
|
||||||
|
.path_filter(|path, _| path.components().count() == 1)
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
|
||||||
|
let resp = test::call_service(&st, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/src/lib.rs").to_srv_request();
|
||||||
|
let resp = test::call_service(&st, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_default_handler_filter() {
|
||||||
|
let st = Files::new("/", ".")
|
||||||
|
.default_handler(|req: ServiceRequest| async {
|
||||||
|
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||||
|
})
|
||||||
|
.path_filter(|path, _| path.extension() == Some("png".as_ref()))
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
|
||||||
|
let resp = test::call_service(&st, req).await;
|
||||||
|
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
let bytes = test::read_body(resp).await;
|
||||||
|
assert_eq!(bytes, web::Bytes::from_static(b"default content"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,35 @@
|
|||||||
use std::fs::{File, Metadata};
|
use std::{
|
||||||
use std::io;
|
fmt,
|
||||||
use std::ops::{Deref, DerefMut};
|
fs::Metadata,
|
||||||
use std::path::{Path, PathBuf};
|
io,
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
ops::{Deref, DerefMut},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
||||||
|
use actix_service::{Service, ServiceFactory};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::{BodyEncoding, SizedStream},
|
body::{self, BoxBody, SizedStream},
|
||||||
|
dev::{
|
||||||
|
AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest,
|
||||||
|
ServiceResponse,
|
||||||
|
},
|
||||||
http::{
|
http::{
|
||||||
header::{
|
header::{
|
||||||
self, Charset, ContentDisposition, DispositionParam, DispositionType,
|
self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
|
||||||
ExtendedValue,
|
DispositionType, ExtendedValue,
|
||||||
},
|
},
|
||||||
ContentEncoding, StatusCode,
|
StatusCode,
|
||||||
},
|
},
|
||||||
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use futures_util::future::{ready, Ready};
|
use futures_core::future::LocalBoxFuture;
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
|
|
||||||
use crate::ChunkedReadFile;
|
|
||||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
@@ -41,7 +48,29 @@ impl Default for Flags {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A file with an associated name.
|
/// A file with an associated name.
|
||||||
#[derive(Debug)]
|
///
|
||||||
|
/// `NamedFile` can be registered as services:
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::App;
|
||||||
|
/// use actix_files::NamedFile;
|
||||||
|
///
|
||||||
|
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let file = NamedFile::open_async("./static/index.html").await?;
|
||||||
|
/// let app = App::new().service(file);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// They can also be returned from handlers:
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::{Responder, get};
|
||||||
|
/// use actix_files::NamedFile;
|
||||||
|
///
|
||||||
|
/// #[get("/")]
|
||||||
|
/// async fn index() -> impl Responder {
|
||||||
|
/// NamedFile::open_async("./static/index.html").await
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
pub struct NamedFile {
|
pub struct NamedFile {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
file: File,
|
file: File,
|
||||||
@@ -54,6 +83,39 @@ pub struct NamedFile {
|
|||||||
pub(crate) encoding: Option<ContentEncoding>,
|
pub(crate) encoding: Option<ContentEncoding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for NamedFile {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("NamedFile")
|
||||||
|
.field("path", &self.path)
|
||||||
|
.field(
|
||||||
|
"file",
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
{
|
||||||
|
&"tokio_uring::File"
|
||||||
|
},
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
{
|
||||||
|
&self.file
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.field("modified", &self.modified)
|
||||||
|
.field("md", &self.md)
|
||||||
|
.field("flags", &self.flags)
|
||||||
|
.field("status_code", &self.status_code)
|
||||||
|
.field("content_type", &self.content_type)
|
||||||
|
.field("content_disposition", &self.content_disposition)
|
||||||
|
.field("encoding", &self.encoding)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
pub(crate) use std::fs::File;
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
pub(crate) use tokio_uring::fs::File;
|
||||||
|
|
||||||
|
use super::chunked;
|
||||||
|
|
||||||
impl NamedFile {
|
impl NamedFile {
|
||||||
/// Creates an instance from a previously opened file.
|
/// Creates an instance from a previously opened file.
|
||||||
///
|
///
|
||||||
@@ -61,8 +123,7 @@ impl NamedFile {
|
|||||||
/// `ContentDisposition` headers.
|
/// `ContentDisposition` headers.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
/// ```ignore
|
||||||
/// ```rust
|
|
||||||
/// use actix_files::NamedFile;
|
/// use actix_files::NamedFile;
|
||||||
/// use std::io::{self, Write};
|
/// use std::io::{self, Write};
|
||||||
/// use std::env;
|
/// use std::env;
|
||||||
@@ -96,6 +157,11 @@ impl NamedFile {
|
|||||||
|
|
||||||
let disposition = match ct.type_() {
|
let disposition = match ct.type_() {
|
||||||
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
||||||
|
mime::APPLICATION => match ct.subtype() {
|
||||||
|
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
||||||
|
name if name == "wasm" => DispositionType::Inline,
|
||||||
|
_ => DispositionType::Attachment,
|
||||||
|
},
|
||||||
_ => DispositionType::Attachment,
|
_ => DispositionType::Attachment,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,7 +184,30 @@ impl NamedFile {
|
|||||||
(ct, cd)
|
(ct, cd)
|
||||||
};
|
};
|
||||||
|
|
||||||
let md = file.metadata()?;
|
let md = {
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
{
|
||||||
|
file.metadata()?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
{
|
||||||
|
use std::os::unix::prelude::{AsRawFd, FromRawFd};
|
||||||
|
|
||||||
|
let fd = file.as_raw_fd();
|
||||||
|
|
||||||
|
// SAFETY: fd is borrowed and lives longer than the unsafe block
|
||||||
|
unsafe {
|
||||||
|
let file = std::fs::File::from_raw_fd(fd);
|
||||||
|
let md = file.metadata();
|
||||||
|
// SAFETY: forget the fd before exiting block in success or error case but don't
|
||||||
|
// run destructor (that would close file handle)
|
||||||
|
std::mem::forget(file);
|
||||||
|
md?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let modified = md.modified().ok();
|
let modified = md.modified().ok();
|
||||||
let encoding = None;
|
let encoding = None;
|
||||||
|
|
||||||
@@ -135,17 +224,45 @@ impl NamedFile {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
/// Attempts to open a file in read-only mode.
|
/// Attempts to open a file in read-only mode.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
/// ```
|
||||||
/// ```rust
|
|
||||||
/// use actix_files::NamedFile;
|
/// use actix_files::NamedFile;
|
||||||
///
|
|
||||||
/// let file = NamedFile::open("foo.txt");
|
/// let file = NamedFile::open("foo.txt");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||||
Self::from_file(File::open(&path)?, path)
|
let file = File::open(&path)?;
|
||||||
|
Self::from_file(file, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to open a file asynchronously in read-only mode.
|
||||||
|
///
|
||||||
|
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
|
||||||
|
/// Otherwise, it will be just like [`open`][Self::open].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use actix_files::NamedFile;
|
||||||
|
/// # async fn open() {
|
||||||
|
/// let file = NamedFile::open_async("foo.txt").await.unwrap();
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn open_async<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||||
|
let file = {
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
{
|
||||||
|
File::open(&path)?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-io-uring")]
|
||||||
|
{
|
||||||
|
File::open(&path).await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::from_file(file, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns reference to the underlying `File` object.
|
/// Returns reference to the underlying `File` object.
|
||||||
@@ -157,13 +274,12 @@ impl NamedFile {
|
|||||||
/// Retrieve the path of this file.
|
/// Retrieve the path of this file.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
/// ```
|
||||||
/// ```rust
|
|
||||||
/// # use std::io;
|
/// # use std::io;
|
||||||
/// use actix_files::NamedFile;
|
/// use actix_files::NamedFile;
|
||||||
///
|
///
|
||||||
/// # fn path() -> io::Result<()> {
|
/// # async fn path() -> io::Result<()> {
|
||||||
/// let file = NamedFile::open("test.txt")?;
|
/// let file = NamedFile::open_async("test.txt").await?;
|
||||||
/// assert_eq!(file.path().as_os_str(), "foo.txt");
|
/// assert_eq!(file.path().as_os_str(), "foo.txt");
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
@@ -189,9 +305,11 @@ impl NamedFile {
|
|||||||
|
|
||||||
/// Set the Content-Disposition for serving this file. This allows
|
/// Set the Content-Disposition for serving this file. This allows
|
||||||
/// changing the inline/attachment disposition as well as the filename
|
/// changing the inline/attachment disposition as well as the filename
|
||||||
/// sent to the peer. By default the disposition is `inline` for text,
|
/// sent to the peer.
|
||||||
/// image, and video content types, and `attachment` otherwise, and
|
///
|
||||||
/// the filename is taken from the path provided in the `open` method
|
/// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
|
||||||
|
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise,
|
||||||
|
/// and the filename is taken from the path provided in the `open` method
|
||||||
/// after converting it to UTF-8 using.
|
/// after converting it to UTF-8 using.
|
||||||
/// [`std::ffi::OsStr::to_string_lossy`]
|
/// [`std::ffi::OsStr::to_string_lossy`]
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -211,6 +329,8 @@ impl NamedFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set content encoding for serving this file
|
/// Set content encoding for serving this file
|
||||||
|
///
|
||||||
|
/// Must be used with [`actix_web::middleware::Compress`] to take effect.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
||||||
self.encoding = Some(enc);
|
self.encoding = Some(enc);
|
||||||
@@ -277,37 +397,31 @@ impl NamedFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an `HttpResponse` with file as a streaming body.
|
/// Creates an `HttpResponse` with file as a streaming body.
|
||||||
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
|
pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> {
|
||||||
if self.status_code != StatusCode::OK {
|
if self.status_code != StatusCode::OK {
|
||||||
let mut res = HttpResponse::build(self.status_code);
|
let mut res = HttpResponse::build(self.status_code);
|
||||||
|
|
||||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||||
let ct = equiv_utf8_text(self.content_type.clone());
|
let ct = equiv_utf8_text(self.content_type.clone());
|
||||||
res.header(header::CONTENT_TYPE, ct.to_string());
|
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||||
} else {
|
} else {
|
||||||
res.header(header::CONTENT_TYPE, self.content_type.to_string());
|
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||||
res.header(
|
res.insert_header((
|
||||||
header::CONTENT_DISPOSITION,
|
header::CONTENT_DISPOSITION,
|
||||||
self.content_disposition.to_string(),
|
self.content_disposition.to_string(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(current_encoding) = self.encoding {
|
if let Some(current_encoding) = self.encoding {
|
||||||
res.encoding(current_encoding);
|
res.encoding(current_encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = ChunkedReadFile {
|
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
|
||||||
size: self.md.len(),
|
|
||||||
offset: 0,
|
|
||||||
file: Some(self.file),
|
|
||||||
fut: None,
|
|
||||||
counter: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Ok(res.streaming(reader));
|
return res.streaming(reader);
|
||||||
}
|
}
|
||||||
|
|
||||||
let etag = if self.flags.contains(Flags::ETAG) {
|
let etag = if self.flags.contains(Flags::ETAG) {
|
||||||
@@ -328,11 +442,11 @@ impl NamedFile {
|
|||||||
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
|
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
|
||||||
(last_modified, req.get_header())
|
(last_modified, req.get_header())
|
||||||
{
|
{
|
||||||
let t1: SystemTime = m.clone().into();
|
let t1: SystemTime = (*m).into();
|
||||||
let t2: SystemTime = since.clone().into();
|
let t2: SystemTime = (*since).into();
|
||||||
|
|
||||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||||
(Ok(t1), Ok(t2)) => t1 > t2,
|
(Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -347,11 +461,11 @@ impl NamedFile {
|
|||||||
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
|
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
|
||||||
(last_modified, req.get_header())
|
(last_modified, req.get_header())
|
||||||
{
|
{
|
||||||
let t1: SystemTime = m.clone().into();
|
let t1: SystemTime = (*m).into();
|
||||||
let t2: SystemTime = since.clone().into();
|
let t2: SystemTime = (*since).into();
|
||||||
|
|
||||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||||
(Ok(t1), Ok(t2)) => t1 <= t2,
|
(Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -362,16 +476,16 @@ impl NamedFile {
|
|||||||
|
|
||||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||||
let ct = equiv_utf8_text(self.content_type.clone());
|
let ct = equiv_utf8_text(self.content_type.clone());
|
||||||
resp.header(header::CONTENT_TYPE, ct.to_string());
|
resp.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||||
} else {
|
} else {
|
||||||
resp.header(header::CONTENT_TYPE, self.content_type.to_string());
|
resp.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||||
resp.header(
|
resp.insert_header((
|
||||||
header::CONTENT_DISPOSITION,
|
header::CONTENT_DISPOSITION,
|
||||||
self.content_disposition.to_string(),
|
self.content_disposition.to_string(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// default compressing
|
// default compressing
|
||||||
@@ -380,14 +494,14 @@ impl NamedFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(lm) = last_modified {
|
if let Some(lm) = last_modified {
|
||||||
resp.header(header::LAST_MODIFIED, lm.to_string());
|
resp.insert_header((header::LAST_MODIFIED, lm.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(etag) = etag {
|
if let Some(etag) = etag {
|
||||||
resp.header(header::ETAG, etag.to_string());
|
resp.insert_header((header::ETAG, etag.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.header(header::ACCEPT_RANGES, "bytes");
|
resp.insert_header((header::ACCEPT_RANGES, "bytes"));
|
||||||
|
|
||||||
let mut length = self.md.len();
|
let mut length = self.md.len();
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
@@ -400,57 +514,35 @@ impl NamedFile {
|
|||||||
offset = ranges[0].start;
|
offset = ranges[0].start;
|
||||||
|
|
||||||
resp.encoding(ContentEncoding::Identity);
|
resp.encoding(ContentEncoding::Identity);
|
||||||
resp.header(
|
resp.insert_header((
|
||||||
header::CONTENT_RANGE,
|
header::CONTENT_RANGE,
|
||||||
format!(
|
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
|
||||||
"bytes {}-{}/{}",
|
));
|
||||||
offset,
|
|
||||||
offset + length - 1,
|
|
||||||
self.md.len()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
|
resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
|
||||||
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
|
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
|
return resp.status(StatusCode::BAD_REQUEST).finish();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if precondition_failed {
|
if precondition_failed {
|
||||||
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
|
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
|
||||||
} else if not_modified {
|
} else if not_modified {
|
||||||
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
|
return resp
|
||||||
|
.status(StatusCode::NOT_MODIFIED)
|
||||||
|
.body(body::None::new())
|
||||||
|
.map_into_boxed_body();
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = ChunkedReadFile {
|
let reader = chunked::new_chunked_read(length, offset, self.file);
|
||||||
offset,
|
|
||||||
size: length,
|
|
||||||
file: Some(self.file),
|
|
||||||
fut: None,
|
|
||||||
counter: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if offset != 0 || length != self.md.len() {
|
if offset != 0 || length != self.md.len() {
|
||||||
resp.status(StatusCode::PARTIAL_CONTENT);
|
resp.status(StatusCode::PARTIAL_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(resp.body(SizedStream::new(length, reader)))
|
resp.body(SizedStream::new(length, reader))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for NamedFile {
|
|
||||||
type Target = File;
|
|
||||||
|
|
||||||
fn deref(&self) -> &File {
|
|
||||||
&self.file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for NamedFile {
|
|
||||||
fn deref_mut(&mut self) -> &mut File {
|
|
||||||
&mut self.file
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,11 +586,77 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Responder for NamedFile {
|
impl Deref for NamedFile {
|
||||||
type Error = Error;
|
type Target = File;
|
||||||
type Future = Ready<Result<HttpResponse, Error>>;
|
|
||||||
|
|
||||||
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
fn deref(&self) -> &Self::Target {
|
||||||
ready(self.into_response(req))
|
&self.file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for NamedFile {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Responder for NamedFile {
|
||||||
|
type Body = BoxBody;
|
||||||
|
|
||||||
|
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||||
|
self.into_response(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceFactory<ServiceRequest> for NamedFile {
|
||||||
|
type Response = ServiceResponse;
|
||||||
|
type Error = Error;
|
||||||
|
type Config = ();
|
||||||
|
type Service = NamedFileService;
|
||||||
|
type InitError = ();
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_service(&self, _: ()) -> Self::Future {
|
||||||
|
let service = NamedFileService {
|
||||||
|
path: self.path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::pin(async move { Ok(service) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NamedFileService {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service<ServiceRequest> for NamedFileService {
|
||||||
|
type Response = ServiceResponse;
|
||||||
|
type Error = Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
actix_service::always_ready!();
|
||||||
|
|
||||||
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
|
let (req, _) = req.into_parts();
|
||||||
|
|
||||||
|
let path = self.path.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
let file = NamedFile::open_async(path).await?;
|
||||||
|
let res = file.into_response(&req);
|
||||||
|
Ok(ServiceResponse::new(req, res))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpServiceFactory for NamedFile {
|
||||||
|
fn register(self, config: &mut AppService) {
|
||||||
|
config.register_service(
|
||||||
|
ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()),
|
||||||
|
None,
|
||||||
|
self,
|
||||||
|
None,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use actix_utils::future::{ready, Ready};
|
||||||
use actix_web::{dev::Payload, FromRequest, HttpRequest};
|
use actix_web::{dev::Payload, FromRequest, HttpRequest};
|
||||||
use futures_util::future::{ready, Ready};
|
|
||||||
|
|
||||||
use crate::error::UriSegmentError;
|
use crate::error::UriSegmentError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct PathBufWrap(PathBuf);
|
pub(crate) struct PathBufWrap(PathBuf);
|
||||||
|
|
||||||
impl FromStr for PathBufWrap {
|
impl FromStr for PathBufWrap {
|
||||||
@@ -21,6 +21,8 @@ impl FromStr for PathBufWrap {
|
|||||||
|
|
||||||
impl PathBufWrap {
|
impl PathBufWrap {
|
||||||
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
|
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
|
||||||
|
///
|
||||||
|
/// Path traversal is guarded by this method.
|
||||||
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
||||||
let mut buf = PathBuf::new();
|
let mut buf = PathBuf::new();
|
||||||
|
|
||||||
@@ -59,7 +61,6 @@ impl AsRef<Path> for PathBufWrap {
|
|||||||
impl FromRequest for PathBufWrap {
|
impl FromRequest for PathBufWrap {
|
||||||
type Error = UriSegmentError;
|
type Error = UriSegmentError;
|
||||||
type Future = Ready<Result<Self, Self::Error>>;
|
type Future = Ready<Result<Self, Self::Error>>;
|
||||||
type Config = ();
|
|
||||||
|
|
||||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||||
ready(req.match_info().path().parse())
|
ready(req.match_info().path().parse())
|
||||||
@@ -116,4 +117,24 @@ mod tests {
|
|||||||
PathBuf::from_iter(vec!["test", ".tt"])
|
PathBuf::from_iter(vec!["test", ".tt"])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn path_traversal() {
|
||||||
|
assert_eq!(
|
||||||
|
PathBufWrap::parse_path("/../README.md", false).unwrap().0,
|
||||||
|
PathBuf::from_iter(vec!["README.md"])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PathBufWrap::parse_path("/../README.md", true).unwrap().0,
|
||||||
|
PathBuf::from_iter(vec!["README.md"])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false)
|
||||||
|
.unwrap()
|
||||||
|
.0,
|
||||||
|
PathBuf::from_iter(vec!["etc/passwd"])
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ pub struct HttpRange {
|
|||||||
pub length: u64,
|
pub length: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
const PREFIX: &str = "bytes=";
|
|
||||||
const PREFIX_LEN: usize = 6;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Display, Error)]
|
#[derive(Debug, Clone, Display, Error)]
|
||||||
#[display(fmt = "Parse HTTP Range failed")]
|
#[display(fmt = "Parse HTTP Range failed")]
|
||||||
pub struct ParseRangeErr(#[error(not(source))] ());
|
pub struct ParseRangeErr(#[error(not(source))] ());
|
||||||
@@ -23,84 +20,16 @@ impl HttpRange {
|
|||||||
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
||||||
/// `size` is full size of response (file).
|
/// `size` is full size of response (file).
|
||||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
|
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
|
||||||
if header.is_empty() {
|
match http_range::HttpRange::parse(header, size) {
|
||||||
return Ok(Vec::new());
|
Ok(ranges) => Ok(ranges
|
||||||
|
.iter()
|
||||||
|
.map(|range| HttpRange {
|
||||||
|
start: range.start,
|
||||||
|
length: range.length,
|
||||||
|
})
|
||||||
|
.collect()),
|
||||||
|
Err(_) => Err(ParseRangeErr(())),
|
||||||
}
|
}
|
||||||
if !header.starts_with(PREFIX) {
|
|
||||||
return Err(ParseRangeErr(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let size_sig = size as i64;
|
|
||||||
let mut no_overlap = false;
|
|
||||||
|
|
||||||
let all_ranges: Vec<Option<HttpRange>> = header[PREFIX_LEN..]
|
|
||||||
.split(',')
|
|
||||||
.map(|x| x.trim())
|
|
||||||
.filter(|x| !x.is_empty())
|
|
||||||
.map(|ra| {
|
|
||||||
let mut start_end_iter = ra.split('-');
|
|
||||||
|
|
||||||
let start_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
|
|
||||||
let end_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
|
|
||||||
|
|
||||||
if start_str.is_empty() {
|
|
||||||
// If no start is specified, end specifies the
|
|
||||||
// range start relative to the end of the file.
|
|
||||||
let mut length: i64 =
|
|
||||||
end_str.parse().map_err(|_| ParseRangeErr(()))?;
|
|
||||||
|
|
||||||
if length > size_sig {
|
|
||||||
length = size_sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(HttpRange {
|
|
||||||
start: (size_sig - length) as u64,
|
|
||||||
length: length as u64,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
let start: i64 = start_str.parse().map_err(|_| ParseRangeErr(()))?;
|
|
||||||
|
|
||||||
if start < 0 {
|
|
||||||
return Err(ParseRangeErr(()));
|
|
||||||
}
|
|
||||||
if start >= size_sig {
|
|
||||||
no_overlap = true;
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let length = if end_str.is_empty() {
|
|
||||||
// If no end is specified, range extends to end of the file.
|
|
||||||
size_sig - start
|
|
||||||
} else {
|
|
||||||
let mut end: i64 =
|
|
||||||
end_str.parse().map_err(|_| ParseRangeErr(()))?;
|
|
||||||
|
|
||||||
if start > end {
|
|
||||||
return Err(ParseRangeErr(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if end >= size_sig {
|
|
||||||
end = size_sig - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
end - start + 1
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(HttpRange {
|
|
||||||
start: start as u64,
|
|
||||||
length: length as u64,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Result<_, _>>()?;
|
|
||||||
|
|
||||||
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
|
|
||||||
|
|
||||||
if no_overlap && ranges.is_empty() {
|
|
||||||
return Err(ParseRangeErr(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ranges)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
use std::{
|
use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
|
||||||
fmt, io,
|
|
||||||
path::PathBuf,
|
|
||||||
rc::Rc,
|
|
||||||
task::{Context, Poll},
|
|
||||||
};
|
|
||||||
|
|
||||||
use actix_service::Service;
|
use actix_service::Service;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
@@ -13,15 +8,26 @@ use actix_web::{
|
|||||||
http::{header, Method},
|
http::{header, Method},
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
};
|
};
|
||||||
use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride,
|
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
|
||||||
NamedFile, PathBufWrap,
|
PathBufWrap, PathFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Assembled file serving service.
|
/// Assembled file serving service.
|
||||||
pub struct FilesService {
|
#[derive(Clone)]
|
||||||
|
pub struct FilesService(pub(crate) Rc<FilesServiceInner>);
|
||||||
|
|
||||||
|
impl Deref for FilesService {
|
||||||
|
type Target = FilesServiceInner;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&*self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FilesServiceInner {
|
||||||
pub(crate) directory: PathBuf,
|
pub(crate) directory: PathBuf,
|
||||||
pub(crate) index: Option<String>,
|
pub(crate) index: Option<String>,
|
||||||
pub(crate) show_index: bool,
|
pub(crate) show_index: bool,
|
||||||
@@ -29,26 +35,56 @@ pub struct FilesService {
|
|||||||
pub(crate) default: Option<HttpService>,
|
pub(crate) default: Option<HttpService>,
|
||||||
pub(crate) renderer: Rc<DirectoryRenderer>,
|
pub(crate) renderer: Rc<DirectoryRenderer>,
|
||||||
pub(crate) mime_override: Option<Rc<MimeOverride>>,
|
pub(crate) mime_override: Option<Rc<MimeOverride>>,
|
||||||
|
pub(crate) path_filter: Option<Rc<PathFilter>>,
|
||||||
pub(crate) file_flags: named::Flags,
|
pub(crate) file_flags: named::Flags,
|
||||||
pub(crate) guards: Option<Rc<dyn Guard>>,
|
pub(crate) guards: Option<Rc<dyn Guard>>,
|
||||||
pub(crate) hidden_files: bool,
|
pub(crate) hidden_files: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilesServiceFuture = Either<
|
impl fmt::Debug for FilesServiceInner {
|
||||||
Ready<Result<ServiceResponse, Error>>,
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
|
f.write_str("FilesServiceInner")
|
||||||
>;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FilesService {
|
impl FilesService {
|
||||||
fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
|
async fn handle_err(
|
||||||
log::debug!("Failed to handle {}: {}", req.path(), e);
|
&self,
|
||||||
|
err: io::Error,
|
||||||
|
req: ServiceRequest,
|
||||||
|
) -> Result<ServiceResponse, Error> {
|
||||||
|
log::debug!("error handling {}: {}", req.path(), err);
|
||||||
|
|
||||||
if let Some(ref mut default) = self.default {
|
if let Some(ref default) = self.default {
|
||||||
Either::Right(default.call(req))
|
default.call(req).await
|
||||||
} else {
|
} else {
|
||||||
Either::Left(ok(req.error_response(e)))
|
Ok(req.error_response(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serve_named_file(
|
||||||
|
&self,
|
||||||
|
req: ServiceRequest,
|
||||||
|
mut named_file: NamedFile,
|
||||||
|
) -> ServiceResponse {
|
||||||
|
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();
|
||||||
|
let res = named_file.into_response(&req);
|
||||||
|
ServiceResponse::new(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
|
||||||
|
let dir = Directory::new(self.directory.clone(), path);
|
||||||
|
|
||||||
|
let (req, _) = req.into_parts();
|
||||||
|
|
||||||
|
(self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for FilesService {
|
impl fmt::Debug for FilesService {
|
||||||
@@ -60,13 +96,11 @@ impl fmt::Debug for FilesService {
|
|||||||
impl Service<ServiceRequest> for FilesService {
|
impl Service<ServiceRequest> for FilesService {
|
||||||
type Response = ServiceResponse;
|
type Response = ServiceResponse;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = FilesServiceFuture;
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
actix_service::always_ready!();
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(&mut self, req: ServiceRequest) -> Self::Future {
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
let is_method_valid = if let Some(guard) = &self.guards {
|
let is_method_valid = if let Some(guard) = &self.guards {
|
||||||
// execute user defined guards
|
// execute user defined guards
|
||||||
(**guard).check(req.head())
|
(**guard).check(req.head())
|
||||||
@@ -75,94 +109,87 @@ impl Service<ServiceRequest> for FilesService {
|
|||||||
matches!(*req.method(), Method::HEAD | Method::GET)
|
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||||
};
|
};
|
||||||
|
|
||||||
if !is_method_valid {
|
let this = self.clone();
|
||||||
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 =
|
Box::pin(async move {
|
||||||
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
|
if !is_method_valid {
|
||||||
Ok(item) => item,
|
return Ok(req.into_response(
|
||||||
Err(e) => return Either::Left(ok(req.error_response(e))),
|
actix_web::HttpResponse::MethodNotAllowed()
|
||||||
};
|
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
|
||||||
|
.body("Request did not meet this resource's requirements."),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// full file path
|
let real_path =
|
||||||
let path = match self.directory.join(&real_path).canonicalize() {
|
match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) {
|
||||||
Ok(path) => path,
|
Ok(item) => item,
|
||||||
Err(e) => return self.handle_err(e, req),
|
Err(e) => return Ok(req.error_response(e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if path.is_dir() {
|
if let Some(filter) = &this.path_filter {
|
||||||
if let Some(ref redir_index) = self.index {
|
if !filter(real_path.as_ref(), req.head()) {
|
||||||
if self.redirect_to_slash && !req.path().ends_with('/') {
|
if let Some(ref default) = this.default {
|
||||||
|
return default.call(req).await;
|
||||||
|
} else {
|
||||||
|
return Ok(
|
||||||
|
req.into_response(actix_web::HttpResponse::NotFound().finish())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// full file path
|
||||||
|
let path = this.directory.join(&real_path);
|
||||||
|
if let Err(err) = path.canonicalize() {
|
||||||
|
return this.handle_err(err, req).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
if this.redirect_to_slash
|
||||||
|
&& !req.path().ends_with('/')
|
||||||
|
&& (this.index.is_some() || this.show_index)
|
||||||
|
{
|
||||||
let redirect_to = format!("{}/", req.path());
|
let redirect_to = format!("{}/", req.path());
|
||||||
|
|
||||||
return Either::Left(ok(req.into_response(
|
return Ok(req.into_response(
|
||||||
HttpResponse::Found()
|
HttpResponse::Found()
|
||||||
.header(header::LOCATION, redirect_to)
|
.insert_header((header::LOCATION, redirect_to))
|
||||||
.body("")
|
.finish(),
|
||||||
.into_body(),
|
));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = path.join(redir_index);
|
match this.index {
|
||||||
|
Some(ref index) => {
|
||||||
match NamedFile::open(path) {
|
let named_path = path.join(index);
|
||||||
|
match NamedFile::open_async(named_path).await {
|
||||||
|
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
||||||
|
Err(_) if this.show_index => Ok(this.show_index(req, path)),
|
||||||
|
Err(err) => this.handle_err(err, req).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None if this.show_index => Ok(this.show_index(req, path)),
|
||||||
|
_ => Ok(ServiceResponse::from_err(
|
||||||
|
FilesError::IsDirectory,
|
||||||
|
req.into_parts().0,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match NamedFile::open_async(&path).await {
|
||||||
Ok(mut named_file) => {
|
Ok(mut named_file) => {
|
||||||
if let Some(ref mime_override) = self.mime_override {
|
if let Some(ref mime_override) = this.mime_override {
|
||||||
let new_disposition =
|
let new_disposition =
|
||||||
mime_override(&named_file.content_type.type_());
|
mime_override(&named_file.content_type.type_());
|
||||||
named_file.content_disposition.disposition = new_disposition;
|
named_file.content_disposition.disposition = new_disposition;
|
||||||
}
|
}
|
||||||
named_file.flags = self.file_flags;
|
named_file.flags = this.file_flags;
|
||||||
|
|
||||||
let (req, _) = req.into_parts();
|
let (req, _) = req.into_parts();
|
||||||
Either::Left(ok(match named_file.into_response(&req) {
|
let res = named_file.into_response(&req);
|
||||||
Ok(item) => ServiceResponse::new(req, item),
|
Ok(ServiceResponse::new(req, res))
|
||||||
Err(e) => ServiceResponse::from_err(e, req),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
Err(e) => self.handle_err(e, req),
|
Err(err) => this.handle_err(err, req).await,
|
||||||
}
|
}
|
||||||
} 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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ use actix_web::{
|
|||||||
App,
|
App,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_web::test]
|
||||||
async fn test_utf8_file_contents() {
|
async fn test_utf8_file_contents() {
|
||||||
// use default ISO-8859-1 encoding
|
// use default ISO-8859-1 encoding
|
||||||
let mut srv =
|
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||||
test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
|
||||||
|
|
||||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||||
let res = test::call_service(&mut srv, req).await;
|
let res = test::call_service(&srv, req).await;
|
||||||
|
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -24,13 +23,12 @@ async fn test_utf8_file_contents() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// prefer UTF-8 encoding
|
// prefer UTF-8 encoding
|
||||||
let mut srv = test::init_service(
|
let srv =
|
||||||
App::new().service(Files::new("/", "./tests").prefer_utf8(true)),
|
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true)))
|
||||||
)
|
.await;
|
||||||
.await;
|
|
||||||
|
|
||||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||||
let res = test::call_service(&mut srv, req).await;
|
let res = test::call_service(&srv, req).await;
|
||||||
|
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
1
actix-files/tests/fixtures/guards/first/index.txt
vendored
Normal file
1
actix-files/tests/fixtures/guards/first/index.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
first
|
||||||
1
actix-files/tests/fixtures/guards/second/index.txt
vendored
Normal file
1
actix-files/tests/fixtures/guards/second/index.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
second
|
||||||
36
actix-files/tests/guard.rs
Normal file
36
actix-files/tests/guard.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use actix_files::Files;
|
||||||
|
use actix_web::{
|
||||||
|
guard::Host,
|
||||||
|
http::StatusCode,
|
||||||
|
test::{self, TestRequest},
|
||||||
|
App,
|
||||||
|
};
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn test_guard_filter() {
|
||||||
|
let srv = test::init_service(
|
||||||
|
App::new()
|
||||||
|
.service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com")))
|
||||||
|
.service(
|
||||||
|
Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/index.txt")
|
||||||
|
.append_header(("Host", "first.com"))
|
||||||
|
.to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
assert_eq!(test::read_body(res).await, Bytes::from("first"));
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/index.txt")
|
||||||
|
.append_header(("Host", "second.com"))
|
||||||
|
.to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
assert_eq!(test::read_body(res).await, Bytes::from("second"));
|
||||||
|
}
|
||||||
1
actix-files/tests/symlink-test.png
Symbolic link
1
actix-files/tests/symlink-test.png
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
test.png
|
||||||
1
actix-files/tests/test.js
Normal file
1
actix-files/tests/test.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// this file is empty.
|
||||||
27
actix-files/tests/traversal.rs
Normal file
27
actix-files/tests/traversal.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use actix_files::Files;
|
||||||
|
use actix_web::{
|
||||||
|
http::StatusCode,
|
||||||
|
test::{self, TestRequest},
|
||||||
|
App,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_directory_traversal_prevention() {
|
||||||
|
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||||
|
|
||||||
|
let req =
|
||||||
|
TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri(
|
||||||
|
"/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd",
|
||||||
|
)
|
||||||
|
.to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/%00/etc/passwd%00").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
@@ -3,6 +3,44 @@
|
|||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.8 - 2021-11-30
|
||||||
|
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||||
|
|
||||||
|
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.7 - 2021-11-22
|
||||||
|
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||||
|
|
||||||
|
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.6 - 2021-11-15
|
||||||
|
* `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
||||||
|
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||||
|
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.5 - 2021-09-09
|
||||||
|
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.4 - 2021-04-02
|
||||||
|
* Added `TestServer::client_headers` method. [#2097]
|
||||||
|
|
||||||
|
[#2097]: https://github.com/actix/actix-web/pull/2097
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.3 - 2021-03-09
|
||||||
|
* No notable changes.
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.2 - 2021-02-10
|
||||||
|
* No notable changes.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.1 - 2021-01-07
|
## 3.0.0-beta.1 - 2021-01-07
|
||||||
* Update `bytes` to `1.0`. [#1813]
|
* Update `bytes` to `1.0`. [#1813]
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-http-test"
|
name = "actix-http-test"
|
||||||
version = "3.0.0-beta.1"
|
version = "3.0.0-beta.8"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Various helpers for Actix applications to use during testing"
|
description = "Various helpers for Actix applications to use during testing"
|
||||||
readme = "README.md"
|
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-web.git"
|
repository = "https://github.com/actix/actix-web.git"
|
||||||
documentation = "https://docs.rs/actix-http-test/"
|
categories = [
|
||||||
categories = ["network-programming", "asynchronous",
|
"network-programming",
|
||||||
"web-programming::http-server",
|
"asynchronous",
|
||||||
"web-programming::websocket"]
|
"web-programming::http-server",
|
||||||
|
"web-programming::websocket",
|
||||||
|
]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
exclude = [".gitignore", ".cargo/config"]
|
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
@@ -26,30 +26,30 @@ path = "src/lib.rs"
|
|||||||
default = []
|
default = []
|
||||||
|
|
||||||
# openssl
|
# openssl
|
||||||
openssl = ["open-ssl", "awc/openssl"]
|
openssl = ["tls-openssl", "awc/openssl"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2.0.0-beta.2"
|
actix-service = "2.0.0"
|
||||||
actix-codec = "0.4.0-beta.1"
|
actix-codec = "0.4.1"
|
||||||
actix-tls = "3.0.0-beta.2"
|
actix-tls = "3.0.0-rc.1"
|
||||||
actix-utils = "3.0.0-beta.1"
|
actix-utils = "3.0.0"
|
||||||
actix-rt = "2.0.0-beta.1"
|
actix-rt = "2.2"
|
||||||
actix-server = "2.0.0-beta.2"
|
actix-server = "2.0.0-rc.1"
|
||||||
awc = "3.0.0-beta.1"
|
awc = { version = "3.0.0-beta.11", default-features = false }
|
||||||
|
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
http = "0.2.2"
|
http = "0.2.5"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
socket2 = "0.3"
|
socket2 = "0.4"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||||
open-ssl = { version = "0.10", package = "openssl", optional = true }
|
tokio = { version = "1.2", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = "4.0.0-beta.1"
|
actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] }
|
||||||
actix-http = "3.0.0-beta.1"
|
actix-http = "3.0.0-beta.14"
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
> Various helpers for Actix applications to use during testing.
|
> Various helpers for Actix applications to use during testing.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://docs.rs/actix-http-test/2.1.0)
|
[](https://docs.rs/actix-http-test/3.0.0-beta.8)
|
||||||

|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||
[](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)
|
<br>
|
||||||
|
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.8)
|
||||||
|
[](https://crates.io/crates/actix-http-test)
|
||||||
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-http-test)
|
- [API Documentation](https://docs.rs/actix-http-test)
|
||||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
- Minimum Supported Rust Version (MSRV): 1.52
|
||||||
- Minimum Supported Rust Version (MSRV): 1.46.0
|
|
||||||
|
|||||||
@@ -4,40 +4,44 @@
|
|||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
|
||||||
use std::sync::mpsc;
|
#[cfg(feature = "openssl")]
|
||||||
use std::{net, thread, time};
|
extern crate tls_openssl as openssl;
|
||||||
|
|
||||||
|
use std::{net, thread, time::Duration};
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||||
use actix_rt::{net::TcpStream, System};
|
use actix_rt::{net::TcpStream, System};
|
||||||
use actix_server::{Server, ServiceFactory};
|
use actix_server::{Server, ServiceFactory};
|
||||||
use awc::{error::PayloadError, ws, Client, ClientRequest, ClientResponse, Connector};
|
use awc::{
|
||||||
|
error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse,
|
||||||
|
Connector,
|
||||||
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::stream::Stream;
|
use futures_core::stream::Stream;
|
||||||
use http::Method;
|
use http::Method;
|
||||||
use socket2::{Domain, Protocol, Socket, Type};
|
use socket2::{Domain, Protocol, Socket, Type};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
/// Start test server
|
/// Start test server.
|
||||||
///
|
///
|
||||||
/// `TestServer` is very simple test server that simplify process of writing
|
/// `TestServer` is very simple test server that simplify process of writing integration tests cases
|
||||||
/// integration tests cases for actix web applications.
|
/// for HTTP applications.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
/// ```no_run
|
||||||
/// ```rust
|
|
||||||
/// use actix_http::HttpService;
|
/// use actix_http::HttpService;
|
||||||
/// use actix_http_test::TestServer;
|
/// use actix_http_test::test_server;
|
||||||
/// use actix_web::{web, App, HttpResponse, Error};
|
/// use actix_web::{web, App, HttpResponse, Error};
|
||||||
///
|
///
|
||||||
/// async fn my_handler() -> Result<HttpResponse, Error> {
|
/// async fn my_handler() -> Result<HttpResponse, Error> {
|
||||||
/// Ok(HttpResponse::Ok().into())
|
/// Ok(HttpResponse::Ok().into())
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[actix_rt::test]
|
/// #[actix_web::test]
|
||||||
/// async fn test_example() {
|
/// async fn test_example() {
|
||||||
/// let mut srv = TestServer::start(
|
/// let mut srv = TestServer::start(||
|
||||||
/// || HttpService::new(
|
/// HttpService::new(
|
||||||
/// App::new().service(
|
/// App::new().service(web::resource("/").to(my_handler))
|
||||||
/// web::resource("/").to(my_handler))
|
|
||||||
/// )
|
/// )
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -51,86 +55,86 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
|
|||||||
test_server_with_addr(tcp, factory).await
|
test_server_with_addr(tcp, factory).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start [`test server`](test_server()) on a concrete Address
|
/// Start [`test server`](test_server()) on an existing address binding.
|
||||||
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||||
tcp: net::TcpListener,
|
tcp: net::TcpListener,
|
||||||
factory: F,
|
factory: F,
|
||||||
) -> TestServer {
|
) -> TestServer {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (started_tx, started_rx) = std::sync::mpsc::channel();
|
||||||
|
let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1);
|
||||||
|
|
||||||
// run server in separate thread
|
// run server in separate thread
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let sys = System::new("actix-test-server");
|
System::new().block_on(async move {
|
||||||
let local_addr = tcp.local_addr().unwrap();
|
let local_addr = tcp.local_addr().unwrap();
|
||||||
|
|
||||||
let srv = Server::build()
|
let srv = Server::build()
|
||||||
.listen("test", tcp, factory)?
|
.workers(1)
|
||||||
.workers(1)
|
.disable_signals()
|
||||||
.disable_signals();
|
.system_exit()
|
||||||
|
.listen("test", tcp, factory)
|
||||||
|
.expect("test server could not be created");
|
||||||
|
|
||||||
sys.block_on(async {
|
let srv = srv.run();
|
||||||
srv.start();
|
started_tx
|
||||||
tx.send((System::current(), local_addr)).unwrap();
|
.send((System::current(), srv.handle(), local_addr))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// drive server loop
|
||||||
|
srv.await.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
sys.run()
|
// notify TestServer that server and system have shut down
|
||||||
|
// all thread managed resources should be dropped at this point
|
||||||
|
let _ = thread_stop_tx.send(());
|
||||||
});
|
});
|
||||||
|
|
||||||
let (system, addr) = rx.recv().unwrap();
|
let (system, server, addr) = started_rx.recv().unwrap();
|
||||||
|
|
||||||
let client = {
|
let client = {
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
let connector = {
|
let connector = {
|
||||||
#[cfg(feature = "openssl")]
|
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||||
{
|
|
||||||
use open_ssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
|
||||||
|
|
||||||
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||||
builder.set_verify(SslVerifyMode::NONE);
|
|
||||||
let _ = builder
|
builder.set_verify(SslVerifyMode::NONE);
|
||||||
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
let _ = builder
|
||||||
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
||||||
Connector::new()
|
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
||||||
.conn_lifetime(time::Duration::from_secs(0))
|
|
||||||
.timeout(time::Duration::from_millis(30000))
|
Connector::new()
|
||||||
.ssl(builder.build())
|
.conn_lifetime(Duration::from_secs(0))
|
||||||
.finish()
|
.timeout(Duration::from_millis(30000))
|
||||||
}
|
.ssl(builder.build())
|
||||||
#[cfg(not(feature = "openssl"))]
|
};
|
||||||
{
|
|
||||||
Connector::new()
|
#[cfg(not(feature = "openssl"))]
|
||||||
.conn_lifetime(time::Duration::from_secs(0))
|
let connector = {
|
||||||
.timeout(time::Duration::from_millis(30000))
|
Connector::new()
|
||||||
.finish()
|
.conn_lifetime(Duration::from_secs(0))
|
||||||
}
|
.timeout(Duration::from_millis(30000))
|
||||||
};
|
};
|
||||||
|
|
||||||
Client::builder().connector(connector).finish()
|
Client::builder().connector(connector).finish()
|
||||||
};
|
};
|
||||||
actix_tls::connect::start_default_resolver().await.unwrap();
|
|
||||||
|
|
||||||
TestServer {
|
TestServer {
|
||||||
addr,
|
server,
|
||||||
client,
|
client,
|
||||||
system,
|
system,
|
||||||
|
addr,
|
||||||
|
thread_stop_rx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get first available unused address
|
|
||||||
pub fn unused_addr() -> net::SocketAddr {
|
|
||||||
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
|
|
||||||
let socket =
|
|
||||||
Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
|
|
||||||
socket.bind(&addr.into()).unwrap();
|
|
||||||
socket.set_reuse_address(true).unwrap();
|
|
||||||
let tcp = socket.into_tcp_listener();
|
|
||||||
tcp.local_addr().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test server controller
|
/// Test server controller
|
||||||
pub struct TestServer {
|
pub struct TestServer {
|
||||||
|
server: actix_server::ServerHandle,
|
||||||
|
client: awc::Client,
|
||||||
|
system: actix_rt::System,
|
||||||
addr: net::SocketAddr,
|
addr: net::SocketAddr,
|
||||||
client: Client,
|
thread_stop_rx: mpsc::Receiver<()>,
|
||||||
system: System,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestServer {
|
impl TestServer {
|
||||||
@@ -148,7 +152,7 @@ impl TestServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct test https server url
|
/// Construct test HTTPS server URL.
|
||||||
pub fn surl(&self, uri: &str) -> String {
|
pub fn surl(&self, uri: &str) -> String {
|
||||||
if uri.starts_with('/') {
|
if uri.starts_with('/') {
|
||||||
format!("https://localhost:{}{}", self.addr.port(), uri)
|
format!("https://localhost:{}{}", self.addr.port(), uri)
|
||||||
@@ -162,7 +166,7 @@ impl TestServer {
|
|||||||
self.client.get(self.url(path.as_ref()).as_str())
|
self.client.get(self.url(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create https `GET` request
|
/// Create HTTPS `GET` request
|
||||||
pub fn sget<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
pub fn sget<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||||
self.client.get(self.surl(path.as_ref()).as_str())
|
self.client.get(self.surl(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
@@ -172,7 +176,7 @@ impl TestServer {
|
|||||||
self.client.post(self.url(path.as_ref()).as_str())
|
self.client.post(self.url(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create https `POST` request
|
/// Create HTTPS `POST` request
|
||||||
pub fn spost<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
pub fn spost<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||||
self.client.post(self.surl(path.as_ref()).as_str())
|
self.client.post(self.surl(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
@@ -182,7 +186,7 @@ impl TestServer {
|
|||||||
self.client.head(self.url(path.as_ref()).as_str())
|
self.client.head(self.url(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create https `HEAD` request
|
/// Create HTTPS `HEAD` request
|
||||||
pub fn shead<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
pub fn shead<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||||
self.client.head(self.surl(path.as_ref()).as_str())
|
self.client.head(self.surl(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
@@ -192,7 +196,7 @@ impl TestServer {
|
|||||||
self.client.put(self.url(path.as_ref()).as_str())
|
self.client.put(self.url(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create https `PUT` request
|
/// Create HTTPS `PUT` request
|
||||||
pub fn sput<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
pub fn sput<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||||
self.client.put(self.surl(path.as_ref()).as_str())
|
self.client.put(self.surl(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
@@ -202,7 +206,7 @@ impl TestServer {
|
|||||||
self.client.patch(self.url(path.as_ref()).as_str())
|
self.client.patch(self.url(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create https `PATCH` request
|
/// Create HTTPS `PATCH` request
|
||||||
pub fn spatch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
pub fn spatch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||||
self.client.patch(self.surl(path.as_ref()).as_str())
|
self.client.patch(self.surl(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
@@ -212,7 +216,7 @@ impl TestServer {
|
|||||||
self.client.delete(self.url(path.as_ref()).as_str())
|
self.client.delete(self.url(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create https `DELETE` request
|
/// Create HTTPS `DELETE` request
|
||||||
pub fn sdelete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
pub fn sdelete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||||
self.client.delete(self.surl(path.as_ref()).as_str())
|
self.client.delete(self.surl(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
@@ -222,12 +226,12 @@ impl TestServer {
|
|||||||
self.client.options(self.url(path.as_ref()).as_str())
|
self.client.options(self.url(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create https `OPTIONS` request
|
/// Create HTTPS `OPTIONS` request
|
||||||
pub fn soptions<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
pub fn soptions<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||||
self.client.options(self.surl(path.as_ref()).as_str())
|
self.client.options(self.surl(path.as_ref()).as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to test http server
|
/// Connect to test HTTP server
|
||||||
pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
|
pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
|
||||||
self.client.request(method, path.as_ref())
|
self.client.request(method, path.as_ref())
|
||||||
}
|
}
|
||||||
@@ -242,33 +246,66 @@ impl TestServer {
|
|||||||
response.body().limit(10_485_760).await
|
response.body().limit(10_485_760).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to websocket server at a given path
|
/// Connect to WebSocket server at a given path.
|
||||||
pub async fn ws_at(
|
pub async fn ws_at(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError>
|
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
|
||||||
{
|
|
||||||
let url = self.url(path);
|
let url = self.url(path);
|
||||||
let connect = self.client.ws(url).connect();
|
let connect = self.client.ws(url).connect();
|
||||||
connect.await.map(|(_, framed)| framed)
|
connect.await.map(|(_, framed)| framed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to a websocket server
|
/// Connect to a WebSocket server.
|
||||||
pub async fn ws(
|
pub async fn ws(
|
||||||
&mut self,
|
&mut self,
|
||||||
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError>
|
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
|
||||||
{
|
|
||||||
self.ws_at("/").await
|
self.ws_at("/").await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop http server
|
/// Get default HeaderMap of Client.
|
||||||
fn stop(&mut self) {
|
///
|
||||||
|
/// Returns Some(&mut HeaderMap) when Client object is unique
|
||||||
|
/// (No other clone of client exists at the same time).
|
||||||
|
pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
|
||||||
|
self.client.headers()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop HTTP server.
|
||||||
|
///
|
||||||
|
/// Waits for spawned `Server` and `System` to (force) shutdown.
|
||||||
|
pub async fn stop(&mut self) {
|
||||||
|
// signal server to stop
|
||||||
|
self.server.stop(false).await;
|
||||||
|
|
||||||
|
// also signal system to stop
|
||||||
|
// though this is handled by `ServerBuilder::exit_system` too
|
||||||
self.system.stop();
|
self.system.stop();
|
||||||
|
|
||||||
|
// wait for thread to be stopped but don't care about result
|
||||||
|
let _ = self.thread_stop_rx.recv().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TestServer {
|
impl Drop for TestServer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.stop()
|
// calls in this Drop impl should be enough to shut down the server, system, and thread
|
||||||
|
// without needing to await anything
|
||||||
|
|
||||||
|
// signal server to stop
|
||||||
|
let _ = self.server.stop(true);
|
||||||
|
|
||||||
|
// signal system to stop
|
||||||
|
self.system.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a localhost socket address with random, unused port.
|
||||||
|
pub fn unused_addr() -> net::SocketAddr {
|
||||||
|
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||||
|
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
|
||||||
|
socket.bind(&addr.into()).unwrap();
|
||||||
|
socket.set_reuse_address(true).unwrap();
|
||||||
|
let tcp = net::TcpListener::from(socket);
|
||||||
|
tcp.local_addr().unwrap()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,304 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
### Added
|
||||||
|
* Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483]
|
||||||
|
* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483]
|
||||||
|
* `Response::map_into_boxed_body`. [#2468]
|
||||||
|
* `body::EitherBody` enum. [#2468]
|
||||||
|
* `body::None` struct. [#2468]
|
||||||
|
* Impl `MessageBody` for `bytestring::ByteString`. [#2468]
|
||||||
|
* `impl Clone for ws::HandshakeError`. [#2468]
|
||||||
|
* `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
|
||||||
|
* `impl Default ` for `ws::Codec`. [#1920]
|
||||||
|
* `header::QualityItem::{max, min}`. [#2486]
|
||||||
|
* `header::Quality::{MAX, MIN}`. [#2486]
|
||||||
|
* `impl Display` for `header::Quality`. [#2486]
|
||||||
|
* `CloneableExtensions` object for use in `on_connect` handlers. [#2327]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Rename `body::BoxBody::{from_body => new}`. [#2468]
|
||||||
|
* Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468]
|
||||||
|
* The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468]
|
||||||
|
* Error types using in service builders now require `Into<Response<BoxBody>>`. [#2468]
|
||||||
|
* `From` implementations on error types now return a `Response<BoxBody>`. [#2468]
|
||||||
|
* `ResponseBuilder::body(B)` now returns `Response<EitherBody<B>>`. [#2468]
|
||||||
|
* `ResponseBuilder::finish()` now returns `Response<EitherBody<()>>`. [#2468]
|
||||||
|
* `on_connect_ext` methods now receive a `CloneableExtensions` object. [#2327]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* `ResponseBuilder::streaming`. [#2468]
|
||||||
|
* `impl Future` for `ResponseBuilder`. [#2468]
|
||||||
|
* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468]
|
||||||
|
* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468]
|
||||||
|
* `impl Copy` for `ws::Codec`. [#1920]
|
||||||
|
* `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486]
|
||||||
|
* `impl TryFrom<u16>` for `header::Quality`. [#2486]
|
||||||
|
* `http` module. Most everything it contained is exported at the crate root. [#2488]
|
||||||
|
|
||||||
|
[#2327]: https://github.com/actix/actix-web/pull/2327
|
||||||
|
[#2483]: https://github.com/actix/actix-web/pull/2483
|
||||||
|
[#2468]: https://github.com/actix/actix-web/pull/2468
|
||||||
|
[#1920]: https://github.com/actix/actix-web/pull/1920
|
||||||
|
[#2486]: https://github.com/actix/actix-web/pull/2486
|
||||||
|
[#2488]: https://github.com/actix/actix-web/pull/2488
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.14 - 2021-11-30
|
||||||
|
### Changed
|
||||||
|
* Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467]
|
||||||
|
* Expose `header::map` module. [#2467]
|
||||||
|
* Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470]
|
||||||
|
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||||
|
|
||||||
|
[#2467]: https://github.com/actix/actix-web/pull/2467
|
||||||
|
[#2470]: https://github.com/actix/actix-web/pull/2470
|
||||||
|
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.13 - 2021-11-22
|
||||||
|
### Added
|
||||||
|
* `body::AnyBody::empty` for quickly creating an empty body. [#2446]
|
||||||
|
* `body::AnyBody::none` for quickly creating a "none" body. [#2456]
|
||||||
|
* `impl Clone` for `body::AnyBody<S> where S: Clone`. [#2448]
|
||||||
|
* `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Rename `body::AnyBody::{Message => Body}`. [#2446]
|
||||||
|
* Rename `body::AnyBody::{from_message => new_boxed}`. [#2448]
|
||||||
|
* Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448]
|
||||||
|
* Rename `body::{BoxAnyBody => BoxBody}`. [#2448]
|
||||||
|
* Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448]
|
||||||
|
* `Encoder::response` now returns `AnyBody<Encoder<B>>`. [#2448]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446]
|
||||||
|
* `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446]
|
||||||
|
* `EncoderError::Boxed`; it is no longer required. [#2446]
|
||||||
|
* `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446]
|
||||||
|
|
||||||
|
[#2446]: https://github.com/actix/actix-web/pull/2446
|
||||||
|
[#2448]: https://github.com/actix/actix-web/pull/2448
|
||||||
|
[#2456]: https://github.com/actix/actix-web/pull/2456
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.12 - 2021-11-15
|
||||||
|
### Changed
|
||||||
|
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* `client` module. [#2425]
|
||||||
|
* `trust-dns` feature. [#2425]
|
||||||
|
|
||||||
|
[#2425]: https://github.com/actix/actix-web/pull/2425
|
||||||
|
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.11 - 2021-10-20
|
||||||
|
### Changed
|
||||||
|
* Updated rustls to v0.20. [#2414]
|
||||||
|
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||||
|
|
||||||
|
[#2414]: https://github.com/actix/actix-web/pull/2414
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.10 - 2021-09-09
|
||||||
|
### Changed
|
||||||
|
* `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377]
|
||||||
|
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364]
|
||||||
|
* Remove `Into<Error>` bound on `Encoder` body types. [#2375]
|
||||||
|
* Fix quality parse error in Accept-Encoding header. [#2344]
|
||||||
|
|
||||||
|
[#2364]: https://github.com/actix/actix-web/pull/2364
|
||||||
|
[#2375]: https://github.com/actix/actix-web/pull/2375
|
||||||
|
[#2344]: https://github.com/actix/actix-web/pull/2344
|
||||||
|
[#2377]: https://github.com/actix/actix-web/pull/2377
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.9 - 2021-08-09
|
||||||
|
### Fixed
|
||||||
|
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.8 - 2021-06-26
|
||||||
|
### Changed
|
||||||
|
* Change compression algorithm features flags. [#2250]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* `downcast` and `downcast_get_type_id` macros. [#2291]
|
||||||
|
|
||||||
|
[#2291]: https://github.com/actix/actix-web/pull/2291
|
||||||
|
[#2250]: https://github.com/actix/actix-web/pull/2250
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.7 - 2021-06-17
|
||||||
|
### Added
|
||||||
|
* Alias `body::Body` as `body::AnyBody`. [#2215]
|
||||||
|
* `BoxAnyBody`: a boxed message body with boxed errors. [#2183]
|
||||||
|
* Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
|
||||||
|
* Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
|
||||||
|
* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
|
||||||
|
* `Response::into_body` that consumes response and returns body type. [#2201]
|
||||||
|
* `impl Default` for `Response`. [#2201]
|
||||||
|
* Add zstd support for `ContentEncoding`. [#2244]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* The `MessageBody` trait now has an associated `Error` type. [#2183]
|
||||||
|
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
|
||||||
|
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
|
||||||
|
* Places in `Response` where `ResponseBody<B>` was received or returned now simply use `B`. [#2201]
|
||||||
|
* `header` mod is now public. [#2171]
|
||||||
|
* `uri` mod is now public. [#2171]
|
||||||
|
* Update `language-tags` to `0.3`.
|
||||||
|
* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
|
||||||
|
* `ResponseBuilder::message_body` now returns a `Result`. [#2201]
|
||||||
|
* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
|
||||||
|
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
|
||||||
|
* Down-casting for `MessageBody` types. [#2183]
|
||||||
|
* `error::Result` alias. [#2201]
|
||||||
|
* Error field from `Response` and `Response::error`. [#2205]
|
||||||
|
* `impl Future` for `Response`. [#2201]
|
||||||
|
* `Response::take_body` and old `Response::into_body` method that casted body type. [#2201]
|
||||||
|
* `InternalError` and all the error types it constructed. [#2215]
|
||||||
|
* Conversion (`impl Into`) of `Response<Body>` and `ResponseBuilder` to `Error`. [#2215]
|
||||||
|
|
||||||
|
[#2171]: https://github.com/actix/actix-web/pull/2171
|
||||||
|
[#2183]: https://github.com/actix/actix-web/pull/2183
|
||||||
|
[#2196]: https://github.com/actix/actix-web/pull/2196
|
||||||
|
[#2201]: https://github.com/actix/actix-web/pull/2201
|
||||||
|
[#2205]: https://github.com/actix/actix-web/pull/2205
|
||||||
|
[#2215]: https://github.com/actix/actix-web/pull/2215
|
||||||
|
[#2253]: https://github.com/actix/actix-web/pull/2253
|
||||||
|
[#2244]: https://github.com/actix/actix-web/pull/2244
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.6 - 2021-04-17
|
||||||
|
### Added
|
||||||
|
* `impl<T: MessageBody> MessageBody for Pin<Box<T>>`. [#2152]
|
||||||
|
* `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
|
||||||
|
* Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
* The type parameter of `Response` no longer has a default. [#2152]
|
||||||
|
* The `Message` variant of `body::Body` is now `Pin<Box<dyn MessageBody>>`. [#2152]
|
||||||
|
* `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152]
|
||||||
|
* Error enum types are marked `#[non_exhaustive]`. [#2161]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* `cookies` feature flag. [#2065]
|
||||||
|
* Top-level `cookies` mod (re-export). [#2065]
|
||||||
|
* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065]
|
||||||
|
* `impl ResponseError for CookieParseError`. [#2065]
|
||||||
|
* Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148]
|
||||||
|
* `ResponseBuilder::json`. [#2148]
|
||||||
|
* `ResponseBuilder::{set_header, header}`. [#2148]
|
||||||
|
* `impl From<serde_json::Value> for Body`. [#2148]
|
||||||
|
* `Response::build_from`. [#2159]
|
||||||
|
* Most of the status code builders on `Response`. [#2159]
|
||||||
|
|
||||||
|
[#2065]: https://github.com/actix/actix-web/pull/2065
|
||||||
|
[#2148]: https://github.com/actix/actix-web/pull/2148
|
||||||
|
[#2152]: https://github.com/actix/actix-web/pull/2152
|
||||||
|
[#2159]: https://github.com/actix/actix-web/pull/2159
|
||||||
|
[#2158]: https://github.com/actix/actix-web/pull/2158
|
||||||
|
[#2161]: https://github.com/actix/actix-web/pull/2161
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.5 - 2021-04-02
|
||||||
|
### Added
|
||||||
|
* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
|
||||||
|
* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
|
||||||
|
* `client::ConnectionIo` trait alias [#2081]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Common typed HTTP headers were moved to actix-web. [2094]
|
||||||
|
* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
|
||||||
|
|
||||||
|
[#2063]: https://github.com/actix/actix-web/pull/2063
|
||||||
|
[#2081]: https://github.com/actix/actix-web/pull/2081
|
||||||
|
[#2094]: https://github.com/actix/actix-web/pull/2094
|
||||||
|
[#2127]: https://github.com/actix/actix-web/pull/2127
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.4 - 2021-03-08
|
||||||
|
### Changed
|
||||||
|
* Feature `cookies` is now optional and disabled by default. [#1981]
|
||||||
|
* `ws::hash_key` now returns array. [#2035]
|
||||||
|
* `ResponseBuilder::json` now takes `impl Serialize`. [#2052]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
|
||||||
|
* `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994]
|
||||||
|
|
||||||
|
[#1981]: https://github.com/actix/actix-web/pull/1981
|
||||||
|
[#1994]: https://github.com/actix/actix-web/pull/1994
|
||||||
|
[#2035]: https://github.com/actix/actix-web/pull/2035
|
||||||
|
[#2052]: https://github.com/actix/actix-web/pull/2052
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.3 - 2021-02-10
|
||||||
|
* No notable changes.
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.0-beta.2 - 2021-02-10
|
||||||
|
### Added
|
||||||
|
* `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869]
|
||||||
|
* `ResponseBuilder::insert_header` method which allows using typed headers. [#1869]
|
||||||
|
* `ResponseBuilder::append_header` method which allows using typed headers. [#1869]
|
||||||
|
* `TestRequest::insert_header` method which allows using typed headers. [#1869]
|
||||||
|
* `ContentEncoding` implements all necessary header traits. [#1912]
|
||||||
|
* `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964]
|
||||||
|
* `HeaderMap::drain` as an efficient draining iterator. [#1964]
|
||||||
|
* Implement `IntoIterator` for owned `HeaderMap`. [#1964]
|
||||||
|
* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed
|
||||||
|
`mime` types. [#1894]
|
||||||
|
* Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std
|
||||||
|
`TryInto` trait. [#1894]
|
||||||
|
* `Extensions::insert` returns Option of replaced item. [#1904]
|
||||||
|
* Remove `HttpResponseBuilder::json2()`. [#1903]
|
||||||
|
* Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903]
|
||||||
|
* `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type. [#1905]
|
||||||
|
* `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905]
|
||||||
|
* Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool
|
||||||
|
is dead. [#1957]
|
||||||
|
* `HeaderMap::len` now returns number of values instead of number of keys. [#1964]
|
||||||
|
* `HeaderMap::insert` now returns iterator of removed values. [#1964]
|
||||||
|
* `HeaderMap::remove` now returns iterator of removed values. [#1964]
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869]
|
||||||
|
* `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869]
|
||||||
|
* `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869]
|
||||||
|
* `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869]
|
||||||
|
* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
|
||||||
|
* `actors` optional feature. [#1969]
|
||||||
|
* `ResponseError` impl for `actix::MailboxError`. [#1969]
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
* Vastly improve docs and add examples for `HeaderMap`. [#1964]
|
||||||
|
|
||||||
|
[#1869]: https://github.com/actix/actix-web/pull/1869
|
||||||
|
[#1894]: https://github.com/actix/actix-web/pull/1894
|
||||||
|
[#1903]: https://github.com/actix/actix-web/pull/1903
|
||||||
|
[#1904]: https://github.com/actix/actix-web/pull/1904
|
||||||
|
[#1905]: https://github.com/actix/actix-web/pull/1905
|
||||||
|
[#1912]: https://github.com/actix/actix-web/pull/1912
|
||||||
|
[#1957]: https://github.com/actix/actix-web/pull/1957
|
||||||
|
[#1964]: https://github.com/actix/actix-web/pull/1964
|
||||||
|
[#1969]: https://github.com/actix/actix-web/pull/1969
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.1 - 2021-01-07
|
## 3.0.0-beta.1 - 2021-01-07
|
||||||
@@ -22,10 +320,19 @@
|
|||||||
* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
||||||
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
||||||
error would return as `ConnectError::SslError`. [#1813]
|
error would return as `ConnectError::SslError`. [#1813]
|
||||||
|
* Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
|
||||||
|
Due to this change `actix_threadpool::BlockingError` type is moved into
|
||||||
|
`actix_http::error` module. [#1878]
|
||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
[#1857]: https://github.com/actix/actix-web/pull/1857
|
[#1857]: https://github.com/actix/actix-web/pull/1857
|
||||||
[#1864]: https://github.com/actix/actix-web/pull/1864
|
[#1864]: https://github.com/actix/actix-web/pull/1864
|
||||||
|
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||||
|
|
||||||
|
|
||||||
|
## 2.2.1 - 2021-08-09
|
||||||
|
### Fixed
|
||||||
|
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||||
|
|
||||||
|
|
||||||
## 2.2.0 - 2020-11-25
|
## 2.2.0 - 2020-11-25
|
||||||
@@ -72,15 +379,14 @@
|
|||||||
* Update actix-connect and actix-tls dependencies.
|
* Update actix-connect and actix-tls dependencies.
|
||||||
|
|
||||||
|
|
||||||
## [2.0.0-beta.3] - 2020-08-14
|
## 2.0.0-beta.3 - 2020-08-14
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
|
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
|
||||||
|
|
||||||
[#1626]: https://github.com/actix/actix-web/pull/1626
|
[#1626]: https://github.com/actix/actix-web/pull/1626
|
||||||
|
|
||||||
|
|
||||||
## [2.0.0-beta.2] - 2020-07-21
|
## 2.0.0-beta.2 - 2020-07-21
|
||||||
### Fixed
|
### Fixed
|
||||||
* Potential UB in h1 decoder using uninitialized memory. [#1614]
|
* Potential UB in h1 decoder using uninitialized memory. [#1614]
|
||||||
|
|
||||||
@@ -91,10 +397,8 @@
|
|||||||
[#1615]: https://github.com/actix/actix-web/pull/1615
|
[#1615]: https://github.com/actix/actix-web/pull/1615
|
||||||
|
|
||||||
|
|
||||||
## [2.0.0-beta.1] - 2020-07-11
|
## 2.0.0-beta.1 - 2020-07-11
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Migrate cookie handling to `cookie` crate. [#1558]
|
* Migrate cookie handling to `cookie` crate. [#1558]
|
||||||
* Update `sha-1` to 0.9. [#1586]
|
* Update `sha-1` to 0.9. [#1586]
|
||||||
* Fix leak in client pool. [#1580]
|
* Fix leak in client pool. [#1580]
|
||||||
@@ -104,33 +408,30 @@
|
|||||||
[#1586]: https://github.com/actix/actix-web/pull/1586
|
[#1586]: https://github.com/actix/actix-web/pull/1586
|
||||||
[#1580]: https://github.com/actix/actix-web/pull/1580
|
[#1580]: https://github.com/actix/actix-web/pull/1580
|
||||||
|
|
||||||
## [2.0.0-alpha.4] - 2020-05-21
|
|
||||||
|
|
||||||
|
## 2.0.0-alpha.4 - 2020-05-21
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Bump minimum supported Rust version to 1.40
|
* Bump minimum supported Rust version to 1.40
|
||||||
* content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439]
|
* content_length function is removed, and you can set Content-Length by calling
|
||||||
|
no_chunking function [#1439]
|
||||||
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||||
`u64` instead of a `usize`.
|
`u64` instead of a `usize`.
|
||||||
* Update `base64` dependency to 0.12
|
* Update `base64` dependency to 0.12
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Support parsing of `SameSite=None` [#1503]
|
* Support parsing of `SameSite=None` [#1503]
|
||||||
|
|
||||||
[#1439]: https://github.com/actix/actix-web/pull/1439
|
[#1439]: https://github.com/actix/actix-web/pull/1439
|
||||||
[#1503]: https://github.com/actix/actix-web/pull/1503
|
[#1503]: https://github.com/actix/actix-web/pull/1503
|
||||||
|
|
||||||
## [2.0.0-alpha.3] - 2020-05-08
|
|
||||||
|
|
||||||
|
## 2.0.0-alpha.3 - 2020-05-08
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Correct spelling of ConnectError::Unresolved [#1487]
|
* Correct spelling of ConnectError::Unresolved [#1487]
|
||||||
* Fix a mistake in the encoding of websocket continuation messages wherein
|
* Fix a mistake in the encoding of websocket continuation messages wherein
|
||||||
Item::FirstText and Item::FirstBinary are each encoded as the other.
|
Item::FirstText and Item::FirstBinary are each encoded as the other.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Implement `std::error::Error` for our custom errors [#1422]
|
* Implement `std::error::Error` for our custom errors [#1422]
|
||||||
* Remove `failure` support for `ResponseError` since that crate
|
* Remove `failure` support for `ResponseError` since that crate
|
||||||
will be deprecated in the near future.
|
will be deprecated in the near future.
|
||||||
@@ -138,338 +439,247 @@
|
|||||||
[#1422]: https://github.com/actix/actix-web/pull/1422
|
[#1422]: https://github.com/actix/actix-web/pull/1422
|
||||||
[#1487]: https://github.com/actix/actix-web/pull/1487
|
[#1487]: https://github.com/actix/actix-web/pull/1487
|
||||||
|
|
||||||
## [2.0.0-alpha.2] - 2020-03-07
|
|
||||||
|
|
||||||
|
## 2.0.0-alpha.2 - 2020-03-07
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
|
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
|
||||||
|
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
|
||||||
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively
|
respectively to improve download speed for awc when downloading large objects. [#1394]
|
||||||
to improve download speed for awc when downloading large objects. [#1394]
|
* client::Connector accepts initial_window_size and initial_connection_window_size
|
||||||
|
HTTP2 configuration. [#1394]
|
||||||
* client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394]
|
|
||||||
|
|
||||||
* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
|
* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
|
||||||
|
|
||||||
[#1394]: https://github.com/actix/actix-web/pull/1394
|
[#1394]: https://github.com/actix/actix-web/pull/1394
|
||||||
[#1395]: https://github.com/actix/actix-web/pull/1395
|
[#1395]: https://github.com/actix/actix-web/pull/1395
|
||||||
|
|
||||||
## [2.0.0-alpha.1] - 2020-02-27
|
|
||||||
|
|
||||||
|
## 2.0.0-alpha.1 - 2020-02-27
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Update the `time` dependency to 0.2.7.
|
* Update the `time` dependency to 0.2.7.
|
||||||
* Moved actors messages support from actix crate, enabled with feature `actors`.
|
* Moved actors messages support from actix crate, enabled with feature `actors`.
|
||||||
* Breaking change: trait MessageBody requires Unpin and accepting Pin<&mut Self> instead of &mut self in the poll_next().
|
* Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of
|
||||||
|
`&mut self` in the poll_next().
|
||||||
* MessageBody is not implemented for &'static [u8] anymore.
|
* MessageBody is not implemented for &'static [u8] anymore.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Allow `SameSite=None` cookies to be sent in a response.
|
* Allow `SameSite=None` cookies to be sent in a response.
|
||||||
|
|
||||||
## [1.0.1] - 2019-12-20
|
|
||||||
|
|
||||||
|
## 1.0.1 - 2019-12-20
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Poll upgrade service's readiness from HTTP service handlers
|
* Poll upgrade service's readiness from HTTP service handlers
|
||||||
|
|
||||||
* Replace brotli with brotli2 #1224
|
* Replace brotli with brotli2 #1224
|
||||||
|
|
||||||
## [1.0.0] - 2019-12-13
|
|
||||||
|
|
||||||
|
## 1.0.0 - 2019-12-13
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Add websockets continuation frame support
|
* Add websockets continuation frame support
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Replace `flate2-xxx` features with `compress`
|
* Replace `flate2-xxx` features with `compress`
|
||||||
|
|
||||||
## [1.0.0-alpha.5] - 2019-12-09
|
|
||||||
|
|
||||||
|
## 1.0.0-alpha.5 - 2019-12-09
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Check `Upgrade` service readiness before calling it
|
* Check `Upgrade` service readiness before calling it
|
||||||
|
* Fix buffer remaining capacity calculation
|
||||||
* Fix buffer remaining capacity calcualtion
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Websockets: Ping and Pong should have binary data #1049
|
* Websockets: Ping and Pong should have binary data #1049
|
||||||
|
|
||||||
## [1.0.0-alpha.4] - 2019-12-08
|
|
||||||
|
|
||||||
|
## 1.0.0-alpha.4 - 2019-12-08
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Add impl ResponseBuilder for Error
|
* Add impl ResponseBuilder for Error
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Use rust based brotli compression library
|
* Use rust based brotli compression library
|
||||||
|
|
||||||
## [1.0.0-alpha.3] - 2019-12-07
|
## 1.0.0-alpha.3 - 2019-12-07
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Migrate to tokio 0.2
|
* Migrate to tokio 0.2
|
||||||
|
|
||||||
* Migrate to `std::future`
|
* Migrate to `std::future`
|
||||||
|
|
||||||
|
|
||||||
## [0.2.11] - 2019-11-06
|
## 0.2.11 - 2019-11-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
|
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
|
||||||
|
* Add an additional `filename*` param in the `Content-Disposition` header of
|
||||||
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
|
`actix_files::NamedFile` to be more compatible. (#1151)
|
||||||
|
|
||||||
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
|
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
|
||||||
|
charset=utf-8` header [#1118]
|
||||||
|
|
||||||
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header #1118
|
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||||
|
|
||||||
|
|
||||||
## [0.2.10] - 2019-09-11
|
## 0.2.10 - 2019-09-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
|
||||||
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead`
|
with `RequestHead`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* h2 will use error response #1080
|
* h2 will use error response #1080
|
||||||
|
|
||||||
* on_connect result isn't added to request extensions for http2 requests #1009
|
* on_connect result isn't added to request extensions for http2 requests #1009
|
||||||
|
|
||||||
|
|
||||||
## [0.2.9] - 2019-08-13
|
## 0.2.9 - 2019-08-13
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
|
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
|
||||||
|
|
||||||
* Update percent-encoding to 2.1
|
* Update percent-encoding to 2.1
|
||||||
|
|
||||||
* Update serde_urlencoded to 0.6.1
|
* Update serde_urlencoded to 0.6.1
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
|
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
|
||||||
|
|
||||||
|
|
||||||
## [0.2.8] - 2019-08-01
|
## 0.2.8 - 2019-08-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Add `rustls` support
|
* Add `rustls` support
|
||||||
|
|
||||||
* Add `Clone` impl for `HeaderMap`
|
* Add `Clone` impl for `HeaderMap`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* awc client panic #1016
|
* awc client panic #1016
|
||||||
|
* Invalid response with compression middleware enabled, but compression-related features
|
||||||
* Invalid response with compression middleware enabled, but compression-related features disabled #997
|
disabled #997
|
||||||
|
|
||||||
|
|
||||||
## [0.2.7] - 2019-07-18
|
## 0.2.7 - 2019-07-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Add support for downcasting response errors #986
|
* Add support for downcasting response errors #986
|
||||||
|
|
||||||
|
|
||||||
## [0.2.6] - 2019-07-17
|
## 0.2.6 - 2019-07-17
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Replace `ClonableService` with local copy
|
* Replace `ClonableService` with local copy
|
||||||
|
|
||||||
* Upgrade `rand` dependency version to 0.7
|
* Upgrade `rand` dependency version to 0.7
|
||||||
|
|
||||||
|
|
||||||
## [0.2.5] - 2019-06-28
|
## 0.2.5 - 2019-06-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
|
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
||||||
|
|
||||||
* Add `Copy` and `Clone` impls for `ws::Codec`
|
* Add `Copy` and `Clone` impls for `ws::Codec`
|
||||||
|
|
||||||
|
|
||||||
## [0.2.4] - 2019-06-16
|
## 0.2.4 - 2019-06-16
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Do not compress NoContent (204) responses #918
|
* Do not compress NoContent (204) responses #918
|
||||||
|
|
||||||
|
|
||||||
## [0.2.3] - 2019-06-02
|
## 0.2.3 - 2019-06-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Debug impl for ResponseBuilder
|
* Debug impl for ResponseBuilder
|
||||||
|
|
||||||
* From SizedStream and BodyStream for Body
|
* From SizedStream and BodyStream for Body
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* SizedStream uses u64
|
* SizedStream uses u64
|
||||||
|
|
||||||
|
|
||||||
## [0.2.2] - 2019-05-29
|
## 0.2.2 - 2019-05-29
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Parse incoming stream before closing stream on disconnect #868
|
* Parse incoming stream before closing stream on disconnect #868
|
||||||
|
|
||||||
|
|
||||||
## [0.2.1] - 2019-05-25
|
## 0.2.1 - 2019-05-25
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Handle socket read disconnect
|
* Handle socket read disconnect
|
||||||
|
|
||||||
|
|
||||||
## [0.2.0] - 2019-05-12
|
## 0.2.0 - 2019-05-12
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Update actix-service to 0.4
|
* Update actix-service to 0.4
|
||||||
|
|
||||||
* Expect and upgrade services accept `ServerConfig` config.
|
* Expect and upgrade services accept `ServerConfig` config.
|
||||||
|
|
||||||
### Deleted
|
### Deleted
|
||||||
|
|
||||||
* `OneRequest` service
|
* `OneRequest` service
|
||||||
|
|
||||||
|
|
||||||
## [0.1.5] - 2019-05-04
|
## 0.1.5 - 2019-05-04
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Clean up response extensions in response pool #817
|
* Clean up response extensions in response pool #817
|
||||||
|
|
||||||
|
|
||||||
## [0.1.4] - 2019-04-24
|
## 0.1.4 - 2019-04-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Allow to render h1 request headers in `Camel-Case`
|
* Allow to render h1 request headers in `Camel-Case`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Read until eof for http/1.0 responses #771
|
* Read until eof for http/1.0 responses #771
|
||||||
|
|
||||||
|
|
||||||
## [0.1.3] - 2019-04-23
|
## 0.1.3 - 2019-04-23
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Fix http client pool management
|
* Fix http client pool management
|
||||||
|
|
||||||
* Fix http client wait queue management #794
|
* Fix http client wait queue management #794
|
||||||
|
|
||||||
|
|
||||||
## [0.1.2] - 2019-04-23
|
## 0.1.2 - 2019-04-23
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Fix BorrowMutError panic in client connector #793
|
* Fix BorrowMutError panic in client connector #793
|
||||||
|
|
||||||
|
|
||||||
## [0.1.1] - 2019-04-19
|
## 0.1.1 - 2019-04-19
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Cookie::max_age() accepts value in seconds
|
* Cookie::max_age() accepts value in seconds
|
||||||
|
|
||||||
* Cookie::max_age_time() accepts value in time::Duration
|
* Cookie::max_age_time() accepts value in time::Duration
|
||||||
|
|
||||||
* Allow to specify server address for client connector
|
* Allow to specify server address for client connector
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0] - 2019-04-16
|
## 0.1.0 - 2019-04-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
|
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* `actix_http::encoding` always available
|
* `actix_http::encoding` always available
|
||||||
|
|
||||||
* use trust-dns-resolver 0.11.0
|
* use trust-dns-resolver 0.11.0
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0-alpha.5] - 2019-04-12
|
## 0.1.0-alpha.5 - 2019-04-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Allow to use custom service for upgrade requests
|
* Allow to use custom service for upgrade requests
|
||||||
|
|
||||||
* Added `h1::SendResponse` future.
|
* Added `h1::SendResponse` future.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* MessageBody::length() renamed to MessageBody::size() for consistency
|
* MessageBody::length() renamed to MessageBody::size() for consistency
|
||||||
|
|
||||||
* ws handshake verification functions take RequestHead instead of Request
|
* ws handshake verification functions take RequestHead instead of Request
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0-alpha.4] - 2019-04-08
|
## 0.1.0-alpha.4 - 2019-04-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Allow to use custom `Expect` handler
|
* Allow to use custom `Expect` handler
|
||||||
|
|
||||||
* Add minimal `std::error::Error` impl for `Error`
|
* Add minimal `std::error::Error` impl for `Error`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* Export IntoHeaderValue
|
* Export IntoHeaderValue
|
||||||
|
|
||||||
* Render error and return as response body
|
* Render error and return as response body
|
||||||
|
* Use thread pool for response body compression
|
||||||
* Use thread pool for response body comression
|
|
||||||
|
|
||||||
### Deleted
|
### Deleted
|
||||||
|
|
||||||
* Removed PayloadBuffer
|
* Removed PayloadBuffer
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0-alpha.3] - 2019-04-02
|
## 0.1.0-alpha.3 - 2019-04-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Warn when an unsealed private cookie isn't valid UTF-8
|
* Warn when an unsealed private cookie isn't valid UTF-8
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Rust 1.31.0 compatibility
|
* Rust 1.31.0 compatibility
|
||||||
|
|
||||||
* Preallocate read buffer for h1 codec
|
* Preallocate read buffer for h1 codec
|
||||||
|
|
||||||
* Detect socket disconnection during protocol selection
|
* Detect socket disconnection during protocol selection
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0-alpha.2] - 2019-03-29
|
## 0.1.0-alpha.2 - 2019-03-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* Added ws::Message::Nop, no-op websockets message
|
* Added ws::Message::Nop, no-op websockets message
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
* Do not use thread pool for decompression if chunk size is smaller than 2048.
|
||||||
* Do not use thread pool for decomression if chunk size is smaller than 2048.
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0-alpha.1] - 2019-03-28
|
## 0.1.0-alpha.1 - 2019-03-28
|
||||||
|
|
||||||
* Initial impl
|
* Initial impl
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.0.0-beta.1"
|
version = "3.0.0-beta.14"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "HTTP primitives for the Actix ecosystem"
|
description = "HTTP primitives for the Actix ecosystem"
|
||||||
readme = "README.md"
|
|
||||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-web.git"
|
repository = "https://github.com/actix/actix-web.git"
|
||||||
documentation = "https://docs.rs/actix-http/"
|
categories = [
|
||||||
categories = ["network-programming", "asynchronous",
|
"network-programming",
|
||||||
"web-programming::http-server",
|
"asynchronous",
|
||||||
"web-programming::websocket"]
|
"web-programming::http-server",
|
||||||
|
"web-programming::websocket",
|
||||||
|
]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
features = ["openssl", "rustls", "compress", "secure-cookies", "actors"]
|
# features that docs.rs will build with
|
||||||
|
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_http"
|
name = "actix_http"
|
||||||
@@ -25,75 +27,79 @@ path = "src/lib.rs"
|
|||||||
default = []
|
default = []
|
||||||
|
|
||||||
# openssl
|
# openssl
|
||||||
openssl = ["actix-tls/openssl"]
|
openssl = ["actix-tls/accept", "actix-tls/openssl"]
|
||||||
|
|
||||||
# rustls support
|
# rustls support
|
||||||
rustls = ["actix-tls/rustls"]
|
rustls = ["actix-tls/accept", "actix-tls/rustls"]
|
||||||
|
|
||||||
# enable compressison support
|
# enable compression support
|
||||||
compress = ["flate2", "brotli2"]
|
compress-brotli = ["brotli2", "__compress"]
|
||||||
|
compress-gzip = ["flate2", "__compress"]
|
||||||
|
compress-zstd = ["zstd", "__compress"]
|
||||||
|
|
||||||
# support for secure cookies
|
# Internal (PRIVATE!) features used to aid testing and cheking feature status.
|
||||||
secure-cookies = ["cookie/secure"]
|
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||||
|
__compress = []
|
||||||
# support for actix Actor messages
|
|
||||||
actors = ["actix"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2.0.0-beta.2"
|
actix-service = "2.0.0"
|
||||||
actix-codec = "0.4.0-beta.1"
|
actix-codec = "0.4.1"
|
||||||
actix-utils = "3.0.0-beta.1"
|
actix-utils = "3.0.0"
|
||||||
actix-rt = "2.0.0-beta.1"
|
actix-rt = "2.2"
|
||||||
actix-threadpool = "0.3.1"
|
|
||||||
actix-tls = "3.0.0-beta.2"
|
|
||||||
actix = { version = "0.11.0-beta.1", optional = true }
|
|
||||||
|
|
||||||
|
ahash = "0.7"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bitflags = "1.2"
|
bitflags = "1.2"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
cookie = { version = "0.14.1", features = ["percent-encode"] }
|
|
||||||
copyless = "0.1.4"
|
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
either = "1.5.3"
|
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-channel = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||||
futures-util = { version = "0.3.7", default-features = false, features = ["sink"] }
|
h2 = "0.3.1"
|
||||||
fxhash = "0.2.1"
|
http = "0.2.5"
|
||||||
h2 = "0.3.0"
|
httparse = "1.5.1"
|
||||||
http = "0.2.2"
|
httpdate = "1.0.1"
|
||||||
httparse = "1.3"
|
|
||||||
indexmap = "1.3"
|
|
||||||
itoa = "0.4"
|
itoa = "0.4"
|
||||||
lazy_static = "1.4"
|
language-tags = "0.3"
|
||||||
language-tags = "0.2"
|
local-channel = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project = "1.0.0"
|
pin-project = "1.0.0"
|
||||||
|
pin-project-lite = "0.2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1.3"
|
|
||||||
serde = "1.0"
|
|
||||||
serde_json = "1.0"
|
|
||||||
sha-1 = "0.9"
|
sha-1 = "0.9"
|
||||||
slab = "0.4"
|
smallvec = "1.6.1"
|
||||||
serde_urlencoded = "0.7"
|
|
||||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
# tls
|
||||||
|
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
|
||||||
|
|
||||||
# compression
|
# compression
|
||||||
brotli2 = { version="0.3.2", optional = true }
|
brotli2 = { version="0.3.2", optional = true }
|
||||||
flate2 = { version = "1.0.13", optional = true }
|
flate2 = { version = "1.0.13", optional = true }
|
||||||
|
zstd = { version = "0.9", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-server = "2.0.0-beta.2"
|
actix-server = "2.0.0-rc.1"
|
||||||
actix-http-test = { version = "3.0.0-beta.1", features = ["openssl"] }
|
actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] }
|
||||||
actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] }
|
actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] }
|
||||||
criterion = "0.3"
|
async-stream = "0.3"
|
||||||
env_logger = "0.7"
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
serde_derive = "1.0"
|
env_logger = "0.9"
|
||||||
open-ssl = { version="0.10", package = "openssl" }
|
rcgen = "0.8"
|
||||||
rust-tls = { version="0.19", package = "rustls" }
|
regex = "1.3"
|
||||||
|
rustls-pemfile = "0.2"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
static_assertions = "1"
|
||||||
|
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||||
|
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||||
|
tokio = { version = "1.2", features = ["net", "rt"] }
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "ws"
|
||||||
|
required-features = ["rustls"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "write-camel-case"
|
name = "write-camel-case"
|
||||||
@@ -106,3 +112,7 @@ harness = false
|
|||||||
[[bench]]
|
[[bench]]
|
||||||
name = "uninit-headers"
|
name = "uninit-headers"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "quality-value"
|
||||||
|
harness = false
|
||||||
|
|||||||
@@ -3,16 +3,18 @@
|
|||||||
> HTTP primitives for the Actix ecosystem.
|
> HTTP primitives for the Actix ecosystem.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://docs.rs/actix-http/2.2.0)
|
[](https://docs.rs/actix-http/3.0.0-beta.14)
|
||||||

|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||||
[](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)
|
<br />
|
||||||
|
[](https://deps.rs/crate/actix-http/3.0.0-beta.14)
|
||||||
|
[](https://crates.io/crates/actix-http)
|
||||||
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-http)
|
- [API Documentation](https://docs.rs/actix-http)
|
||||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
- Minimum Supported Rust Version (MSRV): 1.52
|
||||||
- Minimum Supported Rust Version (MSRV): 1.46.0
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
|||||||
90
actix-http/benches/quality-value.rs
Normal file
90
actix-http/benches/quality-value.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||||
|
|
||||||
|
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||||
|
|
||||||
|
fn bench_quality_display_impls(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("quality value display impls");
|
||||||
|
|
||||||
|
for i in CODES.iter() {
|
||||||
|
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
|
||||||
|
b.iter(|| _new::Quality(i).to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||||
|
b.iter(|| _naive::Quality(i).to_string())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_quality_display_impls);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
||||||
|
mod _new {
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub struct Quality(pub(crate) u16);
|
||||||
|
|
||||||
|
impl fmt::Display for Quality {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
0 => f.write_str("0"),
|
||||||
|
1000 => f.write_str("1"),
|
||||||
|
|
||||||
|
// some number in the range 1–999
|
||||||
|
x => {
|
||||||
|
f.write_str("0.")?;
|
||||||
|
|
||||||
|
// this implementation avoids string allocation otherwise required
|
||||||
|
// for `.trim_end_matches('0')`
|
||||||
|
|
||||||
|
if x < 10 {
|
||||||
|
f.write_str("00")?;
|
||||||
|
// 0 is handled so it's not possible to have a trailing 0, we can just return
|
||||||
|
itoa::fmt(f, x)
|
||||||
|
} else if x < 100 {
|
||||||
|
f.write_str("0")?;
|
||||||
|
if x % 10 == 0 {
|
||||||
|
// trailing 0, divide by 10 and write
|
||||||
|
itoa::fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa::fmt(f, x)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// x is in range 101–999
|
||||||
|
|
||||||
|
if x % 100 == 0 {
|
||||||
|
// two trailing 0s, divide by 100 and write
|
||||||
|
itoa::fmt(f, x / 100)
|
||||||
|
} else if x % 10 == 0 {
|
||||||
|
// one trailing 0, divide by 10 and write
|
||||||
|
itoa::fmt(f, x / 10)
|
||||||
|
} else {
|
||||||
|
itoa::fmt(f, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod _naive {
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub struct Quality(pub(crate) u16);
|
||||||
|
|
||||||
|
impl fmt::Display for Quality {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
0 => f.write_str("0"),
|
||||||
|
1000 => f.write_str("1"),
|
||||||
|
|
||||||
|
x => {
|
||||||
|
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,12 +78,12 @@ impl HeaderIndex {
|
|||||||
// test cases taken from:
|
// test cases taken from:
|
||||||
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
|
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
|
||||||
|
|
||||||
const REQ_SHORT: &'static [u8] = b"\
|
const REQ_SHORT: &[u8] = b"\
|
||||||
GET / HTTP/1.0\r\n\
|
GET / HTTP/1.0\r\n\
|
||||||
Host: example.com\r\n\
|
Host: example.com\r\n\
|
||||||
Cookie: session=60; user_id=1\r\n\r\n";
|
Cookie: session=60; user_id=1\r\n\r\n";
|
||||||
|
|
||||||
const REQ: &'static [u8] = b"\
|
const REQ: &[u8] = b"\
|
||||||
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
|
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
|
||||||
Host: www.kittyhell.com\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\
|
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\
|
||||||
@@ -119,6 +119,8 @@ mod _original {
|
|||||||
use std::mem::MaybeUninit;
|
use std::mem::MaybeUninit;
|
||||||
|
|
||||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||||
|
#![allow(clippy::uninit_assumed_init)]
|
||||||
|
|
||||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ fn bench_write_camel_case(c: &mut Criterion) {
|
|||||||
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
|
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let mut buf = black_box([0; 24]);
|
let mut buf = black_box([0; 24]);
|
||||||
_new::write_camel_case(black_box(bts), &mut buf)
|
let len = black_box(bts.len());
|
||||||
|
_new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -30,9 +31,12 @@ criterion_group!(benches, bench_write_camel_case);
|
|||||||
criterion_main!(benches);
|
criterion_main!(benches);
|
||||||
|
|
||||||
mod _new {
|
mod _new {
|
||||||
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
|
||||||
// first copy entire (potentially wrong) slice to output
|
// first copy entire (potentially wrong) slice to output
|
||||||
buffer[..value.len()].copy_from_slice(value);
|
let buffer = unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len);
|
||||||
|
std::slice::from_raw_parts_mut(buf, len)
|
||||||
|
};
|
||||||
|
|
||||||
let mut iter = value.iter();
|
let mut iter = value.iter();
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
use std::{env, io};
|
use std::io;
|
||||||
|
|
||||||
use actix_http::{Error, HttpService, Request, Response};
|
use actix_http::{Error, HttpService, Request, Response, StatusCode};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt as _;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use log::info;
|
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
env::set_var("RUST_LOG", "echo=info");
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
Server::build()
|
Server::build()
|
||||||
.bind("echo", "127.0.0.1:8080", || {
|
.bind("echo", ("127.0.0.1", 8080), || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.client_timeout(1000)
|
.client_timeout(1000)
|
||||||
.client_disconnect(1000)
|
.client_disconnect(1000)
|
||||||
@@ -23,10 +21,14 @@ async fn main() -> io::Result<()> {
|
|||||||
body.extend_from_slice(&item?);
|
body.extend_from_slice(&item?);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("request body: {:?}", body);
|
log::info!("request body: {:?}", body);
|
||||||
|
|
||||||
Ok::<_, Error>(
|
Ok::<_, Error>(
|
||||||
Response::Ok()
|
Response::build(StatusCode::OK)
|
||||||
.header("x-head", HeaderValue::from_static("dummy value!"))
|
.insert_header((
|
||||||
|
"x-head",
|
||||||
|
HeaderValue::from_static("dummy value!"),
|
||||||
|
))
|
||||||
.body(body),
|
.body(body),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
use std::{env, io};
|
use std::io;
|
||||||
|
|
||||||
use actix_http::http::HeaderValue;
|
use actix_http::{
|
||||||
use actix_http::{Error, HttpService, Request, Response};
|
body::MessageBody, header::HeaderValue, Error, HttpService, Request, Response,
|
||||||
|
StatusCode,
|
||||||
|
};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt as _;
|
||||||
use log::info;
|
|
||||||
|
|
||||||
async fn handle_request(mut req: Request) -> Result<Response, Error> {
|
async fn handle_request(mut req: Request) -> Result<Response<impl MessageBody>, Error> {
|
||||||
let mut body = BytesMut::new();
|
let mut body = BytesMut::new();
|
||||||
while let Some(item) = req.payload().next().await {
|
while let Some(item) = req.payload().next().await {
|
||||||
body.extend_from_slice(&item?)
|
body.extend_from_slice(&item?)
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("request body: {:?}", body);
|
log::info!("request body: {:?}", body);
|
||||||
Ok(Response::Ok()
|
|
||||||
.header("x-head", HeaderValue::from_static("dummy value!"))
|
Ok(Response::build(StatusCode::OK)
|
||||||
|
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||||
.body(body))
|
.body(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
env::set_var("RUST_LOG", "echo=info");
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
Server::build()
|
Server::build()
|
||||||
.bind("echo", "127.0.0.1:8080", || {
|
.bind("echo", ("127.0.0.1", 8080), || {
|
||||||
HttpService::build().finish(handle_request).tcp()
|
HttpService::build().finish(handle_request).tcp()
|
||||||
})?
|
})?
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
use std::{env, io};
|
use std::{convert::Infallible, io};
|
||||||
|
|
||||||
use actix_http::{HttpService, Response};
|
use actix_http::{HttpService, Response, StatusCode};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use futures_util::future;
|
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use log::info;
|
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
env::set_var("RUST_LOG", "hello_world=info");
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
Server::build()
|
Server::build()
|
||||||
.bind("hello-world", "127.0.0.1:8080", || {
|
.bind("hello-world", ("127.0.0.1", 8080), || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.client_timeout(1000)
|
.client_timeout(1000)
|
||||||
.client_disconnect(1000)
|
.client_disconnect(1000)
|
||||||
.finish(|_req| {
|
.finish(|req| async move {
|
||||||
info!("{:?}", _req);
|
log::info!("{:?}", req);
|
||||||
let mut res = Response::Ok();
|
|
||||||
res.header("x-head", HeaderValue::from_static("dummy value!"));
|
let mut res = Response::build(StatusCode::OK);
|
||||||
future::ok::<_, ()>(res.body("Hello world!"))
|
res.insert_header((
|
||||||
|
"x-head",
|
||||||
|
HeaderValue::from_static("dummy value!"),
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok::<_, Infallible>(res.body("Hello world!"))
|
||||||
})
|
})
|
||||||
.tcp()
|
.tcp()
|
||||||
})?
|
})?
|
||||||
|
|||||||
40
actix-http/examples/streaming-error.rs
Normal file
40
actix-http/examples/streaming-error.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
//! Example showing response body (chunked) stream erroring.
|
||||||
|
//!
|
||||||
|
//! Test using `nc` or `curl`.
|
||||||
|
//! ```sh
|
||||||
|
//! $ curl -vN 127.0.0.1:8080
|
||||||
|
//! $ echo 'GET / HTTP/1.1\n\n' | nc 127.0.0.1 8080
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::{convert::Infallible, io, time::Duration};
|
||||||
|
|
||||||
|
use actix_http::{body::BodyStream, HttpService, Response};
|
||||||
|
use actix_server::Server;
|
||||||
|
use async_stream::stream;
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
#[actix_rt::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
|
Server::build()
|
||||||
|
.bind("streaming-error", ("127.0.0.1", 8080), || {
|
||||||
|
HttpService::build()
|
||||||
|
.finish(|req| async move {
|
||||||
|
log::info!("{:?}", req);
|
||||||
|
let res = Response::ok();
|
||||||
|
|
||||||
|
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
|
||||||
|
yield Ok(Bytes::from("123"));
|
||||||
|
yield Ok(Bytes::from("456"));
|
||||||
|
|
||||||
|
actix_rt::time::sleep(Duration::from_millis(1000)).await;
|
||||||
|
|
||||||
|
yield Err(io::Error::new(io::ErrorKind::Other, ""));
|
||||||
|
})))
|
||||||
|
})
|
||||||
|
.tcp()
|
||||||
|
})?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
115
actix-http/examples/ws.rs
Normal file
115
actix-http/examples/ws.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
//! Sets up a WebSocket server over TCP and TLS.
|
||||||
|
//! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames.
|
||||||
|
|
||||||
|
extern crate tls_rustls as rustls;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_codec::Encoder;
|
||||||
|
use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
|
||||||
|
use actix_rt::time::{interval, Interval};
|
||||||
|
use actix_server::Server;
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use bytestring::ByteString;
|
||||||
|
use futures_core::{ready, Stream};
|
||||||
|
|
||||||
|
#[actix_rt::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
|
Server::build()
|
||||||
|
.bind("tcp", ("127.0.0.1", 8080), || {
|
||||||
|
HttpService::build().h1(handler).tcp()
|
||||||
|
})?
|
||||||
|
.bind("tls", ("127.0.0.1", 8443), || {
|
||||||
|
HttpService::build().finish(handler).rustls(tls_config())
|
||||||
|
})?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
|
||||||
|
log::info!("handshaking");
|
||||||
|
let mut res = ws::handshake(req.head())?;
|
||||||
|
|
||||||
|
// handshake will always fail under HTTP/2
|
||||||
|
|
||||||
|
log::info!("responding");
|
||||||
|
Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Heartbeat {
|
||||||
|
codec: ws::Codec,
|
||||||
|
interval: Interval,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Heartbeat {
|
||||||
|
fn new(codec: ws::Codec) -> Self {
|
||||||
|
Self {
|
||||||
|
codec,
|
||||||
|
interval: interval(Duration::from_secs(4)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream for Heartbeat {
|
||||||
|
type Item = Result<Bytes, Error>;
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Self::Item>> {
|
||||||
|
log::trace!("poll");
|
||||||
|
|
||||||
|
ready!(self.as_mut().interval.poll_tick(cx));
|
||||||
|
|
||||||
|
let mut buffer = BytesMut::new();
|
||||||
|
|
||||||
|
self.as_mut()
|
||||||
|
.codec
|
||||||
|
.encode(
|
||||||
|
ws::Message::Text(ByteString::from_static("hello world")),
|
||||||
|
&mut buffer,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Poll::Ready(Some(Ok(buffer.freeze())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tls_config() -> rustls::ServerConfig {
|
||||||
|
use std::io::BufReader;
|
||||||
|
|
||||||
|
use rustls::{Certificate, PrivateKey};
|
||||||
|
use rustls_pemfile::{certs, pkcs8_private_keys};
|
||||||
|
|
||||||
|
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||||
|
let cert_file = cert.serialize_pem().unwrap();
|
||||||
|
let key_file = cert.serialize_private_key_pem();
|
||||||
|
|
||||||
|
let cert_file = &mut BufReader::new(cert_file.as_bytes());
|
||||||
|
let key_file = &mut BufReader::new(key_file.as_bytes());
|
||||||
|
|
||||||
|
let cert_chain = certs(cert_file)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(Certificate)
|
||||||
|
.collect();
|
||||||
|
let mut keys = pkcs8_private_keys(key_file).unwrap();
|
||||||
|
|
||||||
|
let mut config = rustls::ServerConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
config.alpn_protocols.push(b"http/1.1".to_vec());
|
||||||
|
config.alpn_protocols.push(b"h2".to_vec());
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
@@ -1,710 +0,0 @@
|
|||||||
use std::pin::Pin;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
use std::{fmt, mem};
|
|
||||||
|
|
||||||
use bytes::{Bytes, BytesMut};
|
|
||||||
use futures_core::{ready, Stream};
|
|
||||||
use pin_project::pin_project;
|
|
||||||
|
|
||||||
use crate::error::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
|
||||||
/// Body size hint
|
|
||||||
pub enum BodySize {
|
|
||||||
None,
|
|
||||||
Empty,
|
|
||||||
Sized(u64),
|
|
||||||
Stream,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BodySize {
|
|
||||||
pub fn is_eof(&self) -> bool {
|
|
||||||
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type that provides this trait can be streamed to a peer.
|
|
||||||
pub trait MessageBody {
|
|
||||||
fn size(&self) -> BodySize;
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>>;
|
|
||||||
|
|
||||||
downcast_get_type_id!();
|
|
||||||
}
|
|
||||||
|
|
||||||
downcast!(MessageBody);
|
|
||||||
|
|
||||||
impl MessageBody for () {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Empty
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
Poll::Ready(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: MessageBody + Unpin> MessageBody for Box<T> {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
self.as_ref().size()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
Pin::new(self.get_mut().as_mut()).poll_next(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project(project = ResponseBodyProj)]
|
|
||||||
pub enum ResponseBody<B> {
|
|
||||||
Body(#[pin] B),
|
|
||||||
Other(Body),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseBody<Body> {
|
|
||||||
pub fn into_body<B>(self) -> ResponseBody<B> {
|
|
||||||
match self {
|
|
||||||
ResponseBody::Body(b) => ResponseBody::Other(b),
|
|
||||||
ResponseBody::Other(b) => ResponseBody::Other(b),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> ResponseBody<B> {
|
|
||||||
pub fn take_body(&mut self) -> ResponseBody<B> {
|
|
||||||
std::mem::replace(self, ResponseBody::Other(Body::None))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B: MessageBody> ResponseBody<B> {
|
|
||||||
pub fn as_ref(&self) -> Option<&B> {
|
|
||||||
if let ResponseBody::Body(ref b) = self {
|
|
||||||
Some(b)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B: MessageBody> MessageBody for ResponseBody<B> {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
match self {
|
|
||||||
ResponseBody::Body(ref body) => body.size(),
|
|
||||||
ResponseBody::Other(ref body) => body.size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
match self.project() {
|
|
||||||
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
|
||||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B: MessageBody> Stream for ResponseBody<B> {
|
|
||||||
type Item = Result<Bytes, Error>;
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Self::Item>> {
|
|
||||||
match self.project() {
|
|
||||||
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
|
||||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents various types of http message body.
|
|
||||||
pub enum Body {
|
|
||||||
/// Empty response. `Content-Length` header is not set.
|
|
||||||
None,
|
|
||||||
/// Zero sized response body. `Content-Length` header is set to `0`.
|
|
||||||
Empty,
|
|
||||||
/// Specific response body.
|
|
||||||
Bytes(Bytes),
|
|
||||||
/// Generic message body.
|
|
||||||
Message(Box<dyn MessageBody + Unpin>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Body {
|
|
||||||
/// Create body from slice (copy)
|
|
||||||
pub fn from_slice(s: &[u8]) -> Body {
|
|
||||||
Body::Bytes(Bytes::copy_from_slice(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create body from generic message body.
|
|
||||||
pub fn from_message<B: MessageBody + Unpin + 'static>(body: B) -> Body {
|
|
||||||
Body::Message(Box::new(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for Body {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
match self {
|
|
||||||
Body::None => BodySize::None,
|
|
||||||
Body::Empty => BodySize::Empty,
|
|
||||||
Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
|
|
||||||
Body::Message(ref body) => body.size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
match self.get_mut() {
|
|
||||||
Body::None => Poll::Ready(None),
|
|
||||||
Body::Empty => Poll::Ready(None),
|
|
||||||
Body::Bytes(ref mut bin) => {
|
|
||||||
let len = bin.len();
|
|
||||||
if len == 0 {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(mem::take(bin))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Body::Message(body) => Pin::new(&mut **body).poll_next(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for Body {
|
|
||||||
fn eq(&self, other: &Body) -> bool {
|
|
||||||
match *self {
|
|
||||||
Body::None => matches!(*other, Body::None),
|
|
||||||
Body::Empty => matches!(*other, Body::Empty),
|
|
||||||
Body::Bytes(ref b) => match *other {
|
|
||||||
Body::Bytes(ref b2) => b == b2,
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
Body::Message(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Body {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
Body::None => write!(f, "Body::None"),
|
|
||||||
Body::Empty => write!(f, "Body::Empty"),
|
|
||||||
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b),
|
|
||||||
Body::Message(_) => write!(f, "Body::Message(_)"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&'static str> for Body {
|
|
||||||
fn from(s: &'static str) -> Body {
|
|
||||||
Body::Bytes(Bytes::from_static(s.as_ref()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&'static [u8]> for Body {
|
|
||||||
fn from(s: &'static [u8]) -> Body {
|
|
||||||
Body::Bytes(Bytes::from_static(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<u8>> for Body {
|
|
||||||
fn from(vec: Vec<u8>) -> Body {
|
|
||||||
Body::Bytes(Bytes::from(vec))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for Body {
|
|
||||||
fn from(s: String) -> Body {
|
|
||||||
s.into_bytes().into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a String> for Body {
|
|
||||||
fn from(s: &'a String) -> Body {
|
|
||||||
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Bytes> for Body {
|
|
||||||
fn from(s: Bytes) -> Body {
|
|
||||||
Body::Bytes(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BytesMut> for Body {
|
|
||||||
fn from(s: BytesMut) -> Body {
|
|
||||||
Body::Bytes(s.freeze())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Value> for Body {
|
|
||||||
fn from(v: serde_json::Value) -> Body {
|
|
||||||
Body::Bytes(v.to_string().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> From<SizedStream<S>> for Body
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
|
|
||||||
{
|
|
||||||
fn from(s: SizedStream<S>) -> Body {
|
|
||||||
Body::from_message(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> From<BodyStream<S>> for Body
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
|
||||||
E: Into<Error> + 'static,
|
|
||||||
{
|
|
||||||
fn from(s: BodyStream<S>) -> Body {
|
|
||||||
Body::from_message(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for Bytes {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for BytesMut {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for &'static str {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(Bytes::from_static(
|
|
||||||
mem::take(self.get_mut()).as_ref(),
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for Vec<u8> {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for String {
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Some(Ok(Bytes::from(
|
|
||||||
mem::take(self.get_mut()).into_bytes(),
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type represent streaming body.
|
|
||||||
/// Response does not contain `content-length` header and appropriate transfer encoding is used.
|
|
||||||
pub struct BodyStream<S: Unpin> {
|
|
||||||
stream: S,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> BodyStream<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
|
||||||
E: Into<Error>,
|
|
||||||
{
|
|
||||||
pub fn new(stream: S) -> Self {
|
|
||||||
BodyStream { stream }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, E> MessageBody for BodyStream<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
|
||||||
E: Into<Error>,
|
|
||||||
{
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Stream
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to pull out the next value of the underlying [`Stream`].
|
|
||||||
///
|
|
||||||
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
|
|
||||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
|
||||||
/// [`Stream`] ends.
|
|
||||||
fn poll_next(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
loop {
|
|
||||||
let stream = &mut self.as_mut().stream;
|
|
||||||
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
|
|
||||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
|
||||||
opt => opt.map(|res| res.map_err(Into::into)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type represent streaming body. This body implementation should be used
|
|
||||||
/// if total size of stream is known. Data get sent as is without using transfer encoding.
|
|
||||||
pub struct SizedStream<S: Unpin> {
|
|
||||||
size: u64,
|
|
||||||
stream: S,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> SizedStream<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, Error>> + Unpin,
|
|
||||||
{
|
|
||||||
pub fn new(size: u64, stream: S) -> Self {
|
|
||||||
SizedStream { size, stream }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> MessageBody for SizedStream<S>
|
|
||||||
where
|
|
||||||
S: Stream<Item = Result<Bytes, Error>> + Unpin,
|
|
||||||
{
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.size as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to pull out the next value of the underlying [`Stream`].
|
|
||||||
///
|
|
||||||
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
|
|
||||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
|
||||||
/// [`Stream`] ends.
|
|
||||||
fn poll_next(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
|
||||||
loop {
|
|
||||||
let stream = &mut self.as_mut().stream;
|
|
||||||
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
|
|
||||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
|
||||||
val => val,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use futures_util::future::poll_fn;
|
|
||||||
use futures_util::pin_mut;
|
|
||||||
use futures_util::stream;
|
|
||||||
|
|
||||||
impl Body {
|
|
||||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
|
||||||
match *self {
|
|
||||||
Body::Bytes(ref bin) => &bin,
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseBody<Body> {
|
|
||||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
|
||||||
match *self {
|
|
||||||
ResponseBody::Body(ref b) => b.get_ref(),
|
|
||||||
ResponseBody::Other(ref b) => b.get_ref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_static_str() {
|
|
||||||
assert_eq!(Body::from("").size(), BodySize::Sized(0));
|
|
||||||
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from("test").get_ref(), b"test");
|
|
||||||
|
|
||||||
assert_eq!("test".size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_static_bytes() {
|
|
||||||
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
|
|
||||||
assert_eq!(
|
|
||||||
Body::from_slice(b"test".as_ref()).size(),
|
|
||||||
BodySize::Sized(4)
|
|
||||||
);
|
|
||||||
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
|
|
||||||
let sb = Bytes::from(&b"test"[..]);
|
|
||||||
pin_mut!(sb);
|
|
||||||
|
|
||||||
assert_eq!(sb.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_vec() {
|
|
||||||
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
|
|
||||||
let test_vec = Vec::from("test");
|
|
||||||
pin_mut!(test_vec);
|
|
||||||
|
|
||||||
assert_eq!(test_vec.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_bytes() {
|
|
||||||
let b = Bytes::from("test");
|
|
||||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
|
||||||
pin_mut!(b);
|
|
||||||
|
|
||||||
assert_eq!(b.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_bytes_mut() {
|
|
||||||
let b = BytesMut::from("test");
|
|
||||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
|
||||||
pin_mut!(b);
|
|
||||||
|
|
||||||
assert_eq!(b.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_string() {
|
|
||||||
let b = "test".to_owned();
|
|
||||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
|
||||||
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(Body::from(&b).get_ref(), b"test");
|
|
||||||
pin_mut!(b);
|
|
||||||
|
|
||||||
assert_eq!(b.size(), BodySize::Sized(4));
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
|
||||||
Some(Bytes::from("test"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_unit() {
|
|
||||||
assert_eq!(().size(), BodySize::Empty);
|
|
||||||
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
|
|
||||||
.await
|
|
||||||
.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_box() {
|
|
||||||
let val = Box::new(());
|
|
||||||
pin_mut!(val);
|
|
||||||
assert_eq!(val.size(), BodySize::Empty);
|
|
||||||
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body_eq() {
|
|
||||||
assert!(
|
|
||||||
Body::Bytes(Bytes::from_static(b"1"))
|
|
||||||
== Body::Bytes(Bytes::from_static(b"1"))
|
|
||||||
);
|
|
||||||
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body_debug() {
|
|
||||||
assert!(format!("{:?}", Body::None).contains("Body::None"));
|
|
||||||
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
|
|
||||||
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_serde_json() {
|
|
||||||
use serde_json::json;
|
|
||||||
assert_eq!(
|
|
||||||
Body::from(serde_json::Value::String("test".into())).size(),
|
|
||||||
BodySize::Sized(6)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Body::from(json!({"test-key":"test-value"})).size(),
|
|
||||||
BodySize::Sized(25)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
mod body_stream {
|
|
||||||
use super::*;
|
|
||||||
//use futures::task::noop_waker;
|
|
||||||
//use futures::stream::once;
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn skips_empty_chunks() {
|
|
||||||
let body = BodyStream::new(stream::iter(
|
|
||||||
["1", "", "2"]
|
|
||||||
.iter()
|
|
||||||
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
|
|
||||||
));
|
|
||||||
pin_mut!(body);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("1")),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("2")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Now it does not compile as it should
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn move_pinned_pointer() {
|
|
||||||
let (sender, receiver) = futures::channel::oneshot::channel();
|
|
||||||
let mut body_stream = Ok(BodyStream::new(once(async {
|
|
||||||
let x = Box::new(0i32);
|
|
||||||
let y = &x;
|
|
||||||
receiver.await.unwrap();
|
|
||||||
let _z = **y;
|
|
||||||
Ok::<_, ()>(Bytes::new())
|
|
||||||
})));
|
|
||||||
|
|
||||||
let waker = noop_waker();
|
|
||||||
let mut context = Context::from_waker(&waker);
|
|
||||||
pin_mut!(body_stream);
|
|
||||||
|
|
||||||
let _ = body_stream.as_mut().unwrap().poll_next(&mut context);
|
|
||||||
sender.send(()).unwrap();
|
|
||||||
let _ = std::mem::replace(&mut body_stream, Err([0; 32])).unwrap().poll_next(&mut context);
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
mod sized_stream {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn skips_empty_chunks() {
|
|
||||||
let body = SizedStream::new(
|
|
||||||
2,
|
|
||||||
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
|
|
||||||
);
|
|
||||||
pin_mut!(body);
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("1")),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.ok(),
|
|
||||||
Some(Bytes::from("2")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_body_casting() {
|
|
||||||
let mut body = String::from("hello cast");
|
|
||||||
let resp_body: &mut dyn MessageBody = &mut body;
|
|
||||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
|
||||||
assert_eq!(body, "hello cast");
|
|
||||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
|
||||||
body.push('!');
|
|
||||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
|
||||||
assert_eq!(body, "hello cast!");
|
|
||||||
let not_body = resp_body.downcast_ref::<()>();
|
|
||||||
assert!(not_body.is_none());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
214
actix-http/src/body/body_stream.rs
Normal file
214
actix-http/src/body/body_stream.rs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
use std::{
|
||||||
|
error::Error as StdError,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::{ready, Stream};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody};
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
/// Streaming response wrapper.
|
||||||
|
///
|
||||||
|
/// Response does not contain `Content-Length` header and appropriate transfer encoding is used.
|
||||||
|
pub struct BodyStream<S> {
|
||||||
|
#[pin]
|
||||||
|
stream: S,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: from_infallible method
|
||||||
|
|
||||||
|
impl<S, E> BodyStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, E>>,
|
||||||
|
E: Into<Box<dyn StdError>> + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(stream: S) -> Self {
|
||||||
|
BodyStream { stream }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> MessageBody for BodyStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, E>>,
|
||||||
|
E: Into<Box<dyn StdError>> + 'static,
|
||||||
|
{
|
||||||
|
type Error = E;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Stream
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to pull out the next value of the underlying [`Stream`].
|
||||||
|
///
|
||||||
|
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
|
||||||
|
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||||
|
/// [`Stream`] ends.
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
loop {
|
||||||
|
let stream = self.as_mut().project().stream;
|
||||||
|
|
||||||
|
let chunk = match ready!(stream.poll_next(cx)) {
|
||||||
|
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||||
|
opt => opt,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Poll::Ready(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{convert::Infallible, time::Duration};
|
||||||
|
|
||||||
|
use actix_rt::{
|
||||||
|
pin,
|
||||||
|
time::{sleep, Sleep},
|
||||||
|
};
|
||||||
|
use actix_utils::future::poll_fn;
|
||||||
|
use derive_more::{Display, Error};
|
||||||
|
use futures_core::ready;
|
||||||
|
use futures_util::{stream, FutureExt as _};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::body::to_bytes;
|
||||||
|
|
||||||
|
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
|
||||||
|
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
|
||||||
|
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
|
||||||
|
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
|
||||||
|
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
|
||||||
|
|
||||||
|
assert_not_impl_all!(BodyStream<stream::Empty<Bytes>>: MessageBody);
|
||||||
|
assert_not_impl_all!(BodyStream<stream::Repeat<Bytes>>: MessageBody);
|
||||||
|
// crate::Error is not Clone
|
||||||
|
assert_not_impl_all!(BodyStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn skips_empty_chunks() {
|
||||||
|
let body = BodyStream::new(stream::iter(
|
||||||
|
["1", "", "2"]
|
||||||
|
.iter()
|
||||||
|
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
|
||||||
|
));
|
||||||
|
pin!(body);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("1")),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("2")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn read_to_bytes() {
|
||||||
|
let body = BodyStream::new(stream::iter(
|
||||||
|
["1", "", "2"]
|
||||||
|
.iter()
|
||||||
|
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
|
||||||
|
}
|
||||||
|
#[derive(Debug, Display, Error)]
|
||||||
|
#[display(fmt = "stream error")]
|
||||||
|
struct StreamErr;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_immediate_error() {
|
||||||
|
let body = BodyStream::new(stream::once(async { Err(StreamErr) }));
|
||||||
|
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_string_error() {
|
||||||
|
// `&'static str` does not impl `Error`
|
||||||
|
// but it does impl `Into<Box<dyn Error>>`
|
||||||
|
|
||||||
|
let body = BodyStream::new(stream::once(async { Err("stringy error") }));
|
||||||
|
assert!(matches!(to_bytes(body).await, Err("stringy error")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_boxed_error() {
|
||||||
|
// `Box<dyn Error>` does not impl `Error`
|
||||||
|
// but it does impl `Into<Box<dyn Error>>`
|
||||||
|
|
||||||
|
let body = BodyStream::new(stream::once(async {
|
||||||
|
Err(Box::<dyn StdError>::from("stringy error"))
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_bytes(body).await.unwrap_err().to_string(),
|
||||||
|
"stringy error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_delayed_error() {
|
||||||
|
let body =
|
||||||
|
BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
|
||||||
|
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[project = TimeDelayStreamProj]
|
||||||
|
enum TimeDelayStream {
|
||||||
|
Start,
|
||||||
|
Sleep { delay: Pin<Box<Sleep>> },
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream for TimeDelayStream {
|
||||||
|
type Item = Result<Bytes, StreamErr>;
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Self::Item>> {
|
||||||
|
match self.as_mut().get_mut() {
|
||||||
|
TimeDelayStream::Start => {
|
||||||
|
let sleep = sleep(Duration::from_millis(1));
|
||||||
|
self.as_mut().set(TimeDelayStream::Sleep {
|
||||||
|
delay: Box::pin(sleep),
|
||||||
|
});
|
||||||
|
cx.waker().wake_by_ref();
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeDelayStream::Sleep { ref mut delay } => {
|
||||||
|
ready!(delay.poll_unpin(cx));
|
||||||
|
self.set(TimeDelayStream::Done);
|
||||||
|
cx.waker().wake_by_ref();
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeDelayStream::Done => Poll::Ready(Some(Err(StreamErr))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = BodyStream::new(TimeDelayStream::Start);
|
||||||
|
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||||
|
}
|
||||||
|
}
|
||||||
80
actix-http/src/body/boxed.rs
Normal file
80
actix-http/src/body/boxed.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use std::{
|
||||||
|
error::Error as StdError,
|
||||||
|
fmt,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody, MessageBodyMapErr};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// A boxed message body with boxed errors.
|
||||||
|
pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>);
|
||||||
|
|
||||||
|
impl BoxBody {
|
||||||
|
/// Boxes a `MessageBody` and any errors it generates.
|
||||||
|
pub fn new<B>(body: B) -> Self
|
||||||
|
where
|
||||||
|
B: MessageBody + 'static,
|
||||||
|
{
|
||||||
|
let body = MessageBodyMapErr::new(body, Into::into);
|
||||||
|
Self(Box::pin(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable pinned reference to the inner message body type.
|
||||||
|
pub fn as_pin_mut(
|
||||||
|
&mut self,
|
||||||
|
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> {
|
||||||
|
self.0.as_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for BoxBody {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("BoxBody(dyn MessageBody)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for BoxBody {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
self.0.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
self.0
|
||||||
|
.as_mut()
|
||||||
|
.poll_next(cx)
|
||||||
|
.map_err(|err| Error::new_body().with_cause(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::body::to_bytes;
|
||||||
|
|
||||||
|
assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin);
|
||||||
|
|
||||||
|
assert_not_impl_all!(BoxBody: Send, Sync, Unpin);
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn nested_boxed_body() {
|
||||||
|
let body = Bytes::from_static(&[1, 2, 3]);
|
||||||
|
let boxed_body = BoxBody::new(BoxBody::new(body));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
to_bytes(boxed_body).await.unwrap(),
|
||||||
|
Bytes::from(vec![1, 2, 3]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
actix-http/src/body/either.rs
Normal file
83
actix-http/src/body/either.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
|
use super::{BodySize, BoxBody, MessageBody};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
#[project = EitherBodyProj]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum EitherBody<L, R = BoxBody> {
|
||||||
|
/// A body of type `L`.
|
||||||
|
Left { #[pin] body: L },
|
||||||
|
|
||||||
|
/// A body of type `R`.
|
||||||
|
Right { #[pin] body: R },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L> EitherBody<L, BoxBody> {
|
||||||
|
/// Creates new `EitherBody` using left variant and boxed right variant.
|
||||||
|
pub fn new(body: L) -> Self {
|
||||||
|
Self::Left { body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, R> EitherBody<L, R> {
|
||||||
|
/// Creates new `EitherBody` using left variant.
|
||||||
|
pub fn left(body: L) -> Self {
|
||||||
|
Self::Left { body }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates new `EitherBody` using right variant.
|
||||||
|
pub fn right(body: R) -> Self {
|
||||||
|
Self::Right { body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, R> MessageBody for EitherBody<L, R>
|
||||||
|
where
|
||||||
|
L: MessageBody + 'static,
|
||||||
|
R: MessageBody + 'static,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
match self {
|
||||||
|
EitherBody::Left { body } => body.size(),
|
||||||
|
EitherBody::Right { body } => body.size(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
match self.project() {
|
||||||
|
EitherBodyProj::Left { body } => body
|
||||||
|
.poll_next(cx)
|
||||||
|
.map_err(|err| Error::new_body().with_cause(err)),
|
||||||
|
EitherBodyProj::Right { body } => body
|
||||||
|
.poll_next(cx)
|
||||||
|
.map_err(|err| Error::new_body().with_cause(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_parameter_inference() {
|
||||||
|
let _body: EitherBody<(), _> = EitherBody::new(());
|
||||||
|
|
||||||
|
let _body: EitherBody<_, ()> = EitherBody::left(());
|
||||||
|
let _body: EitherBody<(), _> = EitherBody::right(());
|
||||||
|
}
|
||||||
|
}
|
||||||
425
actix-http/src/body/message_body.rs
Normal file
425
actix-http/src/body/message_body.rs
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
//! [`MessageBody`] trait and foreign implementations.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
error::Error as StdError,
|
||||||
|
mem,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use futures_core::ready;
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
|
use super::BodySize;
|
||||||
|
|
||||||
|
/// An interface types that can converted to bytes and used as response bodies.
|
||||||
|
// TODO: examples
|
||||||
|
pub trait MessageBody {
|
||||||
|
// TODO: consider this bound to only fmt::Display since the error type is not really used
|
||||||
|
// and there is an impl for Into<Box<StdError>> on String
|
||||||
|
type Error: Into<Box<dyn StdError>>;
|
||||||
|
|
||||||
|
/// Body size hint.
|
||||||
|
fn size(&self) -> BodySize;
|
||||||
|
|
||||||
|
/// Attempt to pull out the next chunk of body bytes.
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod foreign_impls {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl MessageBody for Infallible {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
match *self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
match *self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for () {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
Poll::Ready(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B> MessageBody for Box<B>
|
||||||
|
where
|
||||||
|
B: MessageBody + Unpin,
|
||||||
|
{
|
||||||
|
type Error = B::Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
self.as_ref().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
Pin::new(self.get_mut().as_mut()).poll_next(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B> MessageBody for Pin<Box<B>>
|
||||||
|
where
|
||||||
|
B: MessageBody,
|
||||||
|
{
|
||||||
|
type Error = B::Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
self.as_ref().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
self.as_mut().poll_next(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for &'static [u8] {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let bytes = mem::take(self.get_mut());
|
||||||
|
let bytes = Bytes::from_static(bytes);
|
||||||
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for Bytes {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let bytes = mem::take(self.get_mut());
|
||||||
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for BytesMut {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let bytes = mem::take(self.get_mut()).freeze();
|
||||||
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for Vec<u8> {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let bytes = mem::take(self.get_mut());
|
||||||
|
Poll::Ready(Some(Ok(Bytes::from(bytes))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for &'static str {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let string = mem::take(self.get_mut());
|
||||||
|
let bytes = Bytes::from_static(string.as_bytes());
|
||||||
|
Poll::Ready(Some(Ok(bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for String {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
if self.is_empty() {
|
||||||
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
let string = mem::take(self.get_mut());
|
||||||
|
Poll::Ready(Some(Ok(Bytes::from(string))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for bytestring::ByteString {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
let string = mem::take(self.get_mut());
|
||||||
|
Poll::Ready(Some(Ok(string.into_bytes())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
pub(crate) struct MessageBodyMapErr<B, F> {
|
||||||
|
#[pin]
|
||||||
|
body: B,
|
||||||
|
mapper: Option<F>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B, F, E> MessageBodyMapErr<B, F>
|
||||||
|
where
|
||||||
|
B: MessageBody,
|
||||||
|
F: FnOnce(B::Error) -> E,
|
||||||
|
{
|
||||||
|
pub(crate) fn new(body: B, mapper: F) -> Self {
|
||||||
|
Self {
|
||||||
|
body,
|
||||||
|
mapper: Some(mapper),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B, F, E> MessageBody for MessageBodyMapErr<B, F>
|
||||||
|
where
|
||||||
|
B: MessageBody,
|
||||||
|
F: FnOnce(B::Error) -> E,
|
||||||
|
E: Into<Box<dyn StdError>>,
|
||||||
|
{
|
||||||
|
type Error = E;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
self.body.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
let this = self.as_mut().project();
|
||||||
|
|
||||||
|
match ready!(this.body.poll_next(cx)) {
|
||||||
|
Some(Err(err)) => {
|
||||||
|
let f = self.as_mut().project().mapper.take().unwrap();
|
||||||
|
let mapped_err = (f)(err);
|
||||||
|
Poll::Ready(Some(Err(mapped_err)))
|
||||||
|
}
|
||||||
|
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
|
||||||
|
None => Poll::Ready(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use actix_rt::pin;
|
||||||
|
use actix_utils::future::poll_fn;
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
macro_rules! assert_poll_next {
|
||||||
|
($pin:expr, $exp:expr) => {
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| $pin.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap() // unwrap option
|
||||||
|
.unwrap(), // unwrap result
|
||||||
|
$exp
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! assert_poll_next_none {
|
||||||
|
($pin:expr) => {
|
||||||
|
assert!(poll_fn(|cx| $pin.as_mut().poll_next(cx)).await.is_none());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn boxing_equivalence() {
|
||||||
|
assert_eq!(().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(().size(), Box::new(()).size());
|
||||||
|
assert_eq!(().size(), Box::pin(()).size());
|
||||||
|
|
||||||
|
let pl = Box::new(());
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next_none!(pl);
|
||||||
|
|
||||||
|
let mut pl = Box::pin(());
|
||||||
|
assert_poll_next_none!(pl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_unit() {
|
||||||
|
let pl = ();
|
||||||
|
assert_eq!(pl.size(), BodySize::Sized(0));
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next_none!(pl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_str() {
|
||||||
|
assert_eq!("".size(), BodySize::Sized(0));
|
||||||
|
assert_eq!("test".size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = "test";
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_bytes() {
|
||||||
|
assert_eq!(b"".as_ref().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(b"test".as_ref().size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = b"test".as_ref();
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_vec() {
|
||||||
|
assert_eq!(vec![0; 0].size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(Vec::from("test").size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = Vec::from("test");
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_bytes() {
|
||||||
|
assert_eq!(Bytes::new().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(Bytes::from_static(b"test").size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = Bytes::from_static(b"test");
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_bytes_mut() {
|
||||||
|
assert_eq!(BytesMut::new().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!(BytesMut::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = BytesMut::from("test");
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_string() {
|
||||||
|
assert_eq!(String::new().size(), BodySize::Sized(0));
|
||||||
|
assert_eq!("test".to_owned().size(), BodySize::Sized(4));
|
||||||
|
|
||||||
|
let pl = "test".to_owned();
|
||||||
|
pin!(pl);
|
||||||
|
assert_poll_next!(pl, Bytes::from("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// down-casting used to be done with a method on MessageBody trait
|
||||||
|
// test is kept to demonstrate equivalence of Any trait
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_body_casting() {
|
||||||
|
let mut body = String::from("hello cast");
|
||||||
|
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
|
||||||
|
let resp_body: &mut dyn std::any::Any = &mut body;
|
||||||
|
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||||
|
assert_eq!(body, "hello cast");
|
||||||
|
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||||
|
body.push('!');
|
||||||
|
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||||
|
assert_eq!(body, "hello cast!");
|
||||||
|
let not_body = resp_body.downcast_ref::<()>();
|
||||||
|
assert!(not_body.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
20
actix-http/src/body/mod.rs
Normal file
20
actix-http/src/body/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//! Traits and structures to aid consuming and writing HTTP payloads.
|
||||||
|
|
||||||
|
mod body_stream;
|
||||||
|
mod boxed;
|
||||||
|
mod either;
|
||||||
|
mod message_body;
|
||||||
|
mod none;
|
||||||
|
mod size;
|
||||||
|
mod sized_stream;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use self::body_stream::BodyStream;
|
||||||
|
pub use self::boxed::BoxBody;
|
||||||
|
pub use self::either::EitherBody;
|
||||||
|
pub use self::message_body::MessageBody;
|
||||||
|
pub(crate) use self::message_body::MessageBodyMapErr;
|
||||||
|
pub use self::none::None;
|
||||||
|
pub use self::size::BodySize;
|
||||||
|
pub use self::sized_stream::SizedStream;
|
||||||
|
pub use self::utils::to_bytes;
|
||||||
43
actix-http/src/body/none.rs
Normal file
43
actix-http/src/body/none.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody};
|
||||||
|
|
||||||
|
/// Body type for responses that forbid payloads.
|
||||||
|
///
|
||||||
|
/// Distinct from an empty response which would contain a Content-Length header.
|
||||||
|
///
|
||||||
|
/// For an "empty" body, use `()` or `Bytes::new()`.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct None;
|
||||||
|
|
||||||
|
impl None {
|
||||||
|
/// Constructs new "none" body.
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBody for None {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
Poll::Ready(Option::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
actix-http/src/body/size.rs
Normal file
36
actix-http/src/body/size.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/// Body size hint.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum BodySize {
|
||||||
|
/// Absence of body can be assumed from method or status code.
|
||||||
|
///
|
||||||
|
/// Will skip writing Content-Length header.
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// Known size body.
|
||||||
|
///
|
||||||
|
/// Will write `Content-Length: N` header.
|
||||||
|
Sized(u64),
|
||||||
|
|
||||||
|
/// Unknown size body.
|
||||||
|
///
|
||||||
|
/// Will not write Content-Length header. Can be used with chunked Transfer-Encoding.
|
||||||
|
Stream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BodySize {
|
||||||
|
/// Returns true if size hint indicates omitted or empty body.
|
||||||
|
///
|
||||||
|
/// Streams will return false because it cannot be known without reading the stream.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::body::BodySize;
|
||||||
|
/// assert!(BodySize::None.is_eof());
|
||||||
|
/// assert!(BodySize::Sized(0).is_eof());
|
||||||
|
///
|
||||||
|
/// assert!(!BodySize::Sized(64).is_eof());
|
||||||
|
/// assert!(!BodySize::Stream.is_eof());
|
||||||
|
/// ```
|
||||||
|
pub fn is_eof(&self) -> bool {
|
||||||
|
matches!(self, BodySize::None | BodySize::Sized(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
169
actix-http/src/body/sized_stream.rs
Normal file
169
actix-http/src/body/sized_stream.rs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
use std::{
|
||||||
|
error::Error as StdError,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::{ready, Stream};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody};
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
/// Known sized streaming response wrapper.
|
||||||
|
///
|
||||||
|
/// This body implementation should be used if total size of stream is known. Data is sent as-is
|
||||||
|
/// without using chunked transfer encoding.
|
||||||
|
pub struct SizedStream<S> {
|
||||||
|
size: u64,
|
||||||
|
#[pin]
|
||||||
|
stream: S,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> SizedStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, E>>,
|
||||||
|
E: Into<Box<dyn StdError>> + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(size: u64, stream: S) -> Self {
|
||||||
|
SizedStream { size, stream }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: from_infallible method
|
||||||
|
|
||||||
|
impl<S, E> MessageBody for SizedStream<S>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, E>>,
|
||||||
|
E: Into<Box<dyn StdError>> + 'static,
|
||||||
|
{
|
||||||
|
type Error = E;
|
||||||
|
|
||||||
|
fn size(&self) -> BodySize {
|
||||||
|
BodySize::Sized(self.size as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to pull out the next value of the underlying [`Stream`].
|
||||||
|
///
|
||||||
|
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
|
||||||
|
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||||
|
/// [`Stream`] ends.
|
||||||
|
fn poll_next(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
|
loop {
|
||||||
|
let stream = self.as_mut().project().stream;
|
||||||
|
|
||||||
|
let chunk = match ready!(stream.poll_next(cx)) {
|
||||||
|
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||||
|
val => val,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Poll::Ready(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use actix_rt::pin;
|
||||||
|
use actix_utils::future::poll_fn;
|
||||||
|
use futures_util::stream;
|
||||||
|
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::body::to_bytes;
|
||||||
|
|
||||||
|
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
|
||||||
|
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
|
||||||
|
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
|
||||||
|
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
|
||||||
|
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
|
||||||
|
|
||||||
|
assert_not_impl_all!(SizedStream<stream::Empty<Bytes>>: MessageBody);
|
||||||
|
assert_not_impl_all!(SizedStream<stream::Repeat<Bytes>>: MessageBody);
|
||||||
|
// crate::Error is not Clone
|
||||||
|
assert_not_impl_all!(SizedStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn skips_empty_chunks() {
|
||||||
|
let body = SizedStream::new(
|
||||||
|
2,
|
||||||
|
stream::iter(
|
||||||
|
["1", "", "2"]
|
||||||
|
.iter()
|
||||||
|
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
pin!(body);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("1")),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.ok(),
|
||||||
|
Some(Bytes::from("2")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn read_to_bytes() {
|
||||||
|
let body = SizedStream::new(
|
||||||
|
2,
|
||||||
|
stream::iter(
|
||||||
|
["1", "", "2"]
|
||||||
|
.iter()
|
||||||
|
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_string_error() {
|
||||||
|
// `&'static str` does not impl `Error`
|
||||||
|
// but it does impl `Into<Box<dyn Error>>`
|
||||||
|
|
||||||
|
let body = SizedStream::new(0, stream::once(async { Err("stringy error") }));
|
||||||
|
assert_eq!(to_bytes(body).await, Ok(Bytes::new()));
|
||||||
|
|
||||||
|
let body = SizedStream::new(1, stream::once(async { Err("stringy error") }));
|
||||||
|
assert!(matches!(to_bytes(body).await, Err("stringy error")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn stream_boxed_error() {
|
||||||
|
// `Box<dyn Error>` does not impl `Error`
|
||||||
|
// but it does impl `Into<Box<dyn Error>>`
|
||||||
|
|
||||||
|
let body = SizedStream::new(
|
||||||
|
0,
|
||||||
|
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
|
||||||
|
);
|
||||||
|
assert_eq!(to_bytes(body).await.unwrap(), Bytes::new());
|
||||||
|
|
||||||
|
let body = SizedStream::new(
|
||||||
|
1,
|
||||||
|
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
to_bytes(body).await.unwrap_err().to_string(),
|
||||||
|
"stringy error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
actix-http/src/body/utils.rs
Normal file
78
actix-http/src/body/utils.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use std::task::Poll;
|
||||||
|
|
||||||
|
use actix_rt::pin;
|
||||||
|
use actix_utils::future::poll_fn;
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use futures_core::ready;
|
||||||
|
|
||||||
|
use super::{BodySize, MessageBody};
|
||||||
|
|
||||||
|
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
|
||||||
|
///
|
||||||
|
/// Any errors produced by the body stream are returned immediately.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use actix_http::body::{self, to_bytes};
|
||||||
|
/// use bytes::Bytes;
|
||||||
|
///
|
||||||
|
/// # async fn test_to_bytes() {
|
||||||
|
/// let body = body::None::new();
|
||||||
|
/// let bytes = to_bytes(body).await.unwrap();
|
||||||
|
/// assert!(bytes.is_empty());
|
||||||
|
///
|
||||||
|
/// let body = Bytes::from_static(b"123");
|
||||||
|
/// let bytes = to_bytes(body).await.unwrap();
|
||||||
|
/// assert_eq!(bytes, b"123"[..]);
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
||||||
|
let cap = match body.size() {
|
||||||
|
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
|
||||||
|
BodySize::Sized(size) => size as usize,
|
||||||
|
// good enough first guess for chunk size
|
||||||
|
BodySize::Stream => 32_768,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = BytesMut::with_capacity(cap);
|
||||||
|
|
||||||
|
pin!(body);
|
||||||
|
|
||||||
|
poll_fn(|cx| loop {
|
||||||
|
let body = body.as_mut();
|
||||||
|
|
||||||
|
match ready!(body.poll_next(cx)) {
|
||||||
|
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||||
|
None => return Poll::Ready(Ok(())),
|
||||||
|
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(buf.freeze())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use futures_util::{stream, StreamExt as _};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{body::BodyStream, Error};
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_to_bytes() {
|
||||||
|
let bytes = to_bytes(()).await.unwrap();
|
||||||
|
assert!(bytes.is_empty());
|
||||||
|
|
||||||
|
let body = Bytes::from_static(b"123");
|
||||||
|
let bytes = to_bytes(body).await.unwrap();
|
||||||
|
assert_eq!(bytes, b"123"[..]);
|
||||||
|
|
||||||
|
let stream =
|
||||||
|
stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
|
||||||
|
.map(Ok::<_, Error>);
|
||||||
|
let body = BodyStream::new(stream);
|
||||||
|
let bytes = to_bytes(body).await.unwrap();
|
||||||
|
assert_eq!(bytes, b"123abc"[..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
use std::marker::PhantomData;
|
use std::{fmt, marker::PhantomData, net, rc::Rc};
|
||||||
use std::rc::Rc;
|
|
||||||
use std::{fmt, net};
|
|
||||||
|
|
||||||
use actix_codec::Framed;
|
use actix_codec::Framed;
|
||||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||||
|
|
||||||
use crate::body::MessageBody;
|
use crate::{
|
||||||
use crate::config::{KeepAlive, ServiceConfig};
|
body::{BoxBody, MessageBody},
|
||||||
use crate::error::Error;
|
config::{KeepAlive, ServiceConfig},
|
||||||
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler};
|
extensions::CloneableExtensions,
|
||||||
use crate::h2::H2Service;
|
h1::{self, ExpectHandler, H1Service, UpgradeHandler},
|
||||||
use crate::request::Request;
|
h2::H2Service,
|
||||||
use crate::response::Response;
|
service::HttpService,
|
||||||
use crate::service::HttpService;
|
ConnectCallback, Request, Response,
|
||||||
use crate::{ConnectCallback, Extensions};
|
};
|
||||||
|
|
||||||
/// A HTTP service builder
|
/// A HTTP service builder
|
||||||
///
|
///
|
||||||
@@ -34,7 +32,7 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
|||||||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
{
|
{
|
||||||
@@ -57,17 +55,15 @@ where
|
|||||||
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
|
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Error: Into<Error>,
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
<X::Service as Service<Request>>::Future: 'static,
|
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||||
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
|
||||||
U::Error: fmt::Display,
|
U::Error: fmt::Display,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
<U::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
|
|
||||||
{
|
{
|
||||||
/// Set server keep-alive setting.
|
/// Set server keep-alive setting.
|
||||||
///
|
///
|
||||||
@@ -125,9 +121,8 @@ where
|
|||||||
where
|
where
|
||||||
F: IntoServiceFactory<X1, Request>,
|
F: IntoServiceFactory<X1, Request>,
|
||||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X1::Error: Into<Error>,
|
X1::Error: Into<Response<BoxBody>>,
|
||||||
X1::InitError: fmt::Debug,
|
X1::InitError: fmt::Debug,
|
||||||
<X1::Service as Service<Request>>::Future: 'static,
|
|
||||||
{
|
{
|
||||||
HttpServiceBuilder {
|
HttpServiceBuilder {
|
||||||
keep_alive: self.keep_alive,
|
keep_alive: self.keep_alive,
|
||||||
@@ -148,11 +143,10 @@ where
|
|||||||
/// and this service get called with original request and framed object.
|
/// and this service get called with original request and framed object.
|
||||||
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
|
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
|
||||||
where
|
where
|
||||||
F: IntoServiceFactory<U1, (Request, Framed<T, Codec>)>,
|
F: IntoServiceFactory<U1, (Request, Framed<T, h1::Codec>)>,
|
||||||
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||||
U1::Error: fmt::Display,
|
U1::Error: fmt::Display,
|
||||||
U1::InitError: fmt::Debug,
|
U1::InitError: fmt::Debug,
|
||||||
<U1::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
|
|
||||||
{
|
{
|
||||||
HttpServiceBuilder {
|
HttpServiceBuilder {
|
||||||
keep_alive: self.keep_alive,
|
keep_alive: self.keep_alive,
|
||||||
@@ -174,7 +168,7 @@ where
|
|||||||
/// and handlers.
|
/// and handlers.
|
||||||
pub fn on_connect_ext<F>(mut self, f: F) -> Self
|
pub fn on_connect_ext<F>(mut self, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&T, &mut Extensions) + 'static,
|
F: Fn(&T, &mut CloneableExtensions) + 'static,
|
||||||
{
|
{
|
||||||
self.on_connect_ext = Some(Rc::new(f));
|
self.on_connect_ext = Some(Rc::new(f));
|
||||||
self
|
self
|
||||||
@@ -185,7 +179,7 @@ where
|
|||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
F: IntoServiceFactory<S, Request>,
|
F: IntoServiceFactory<S, Request>,
|
||||||
S::Error: Into<Error>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
{
|
{
|
||||||
@@ -206,12 +200,12 @@ where
|
|||||||
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
||||||
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
|
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
|
||||||
where
|
where
|
||||||
B: MessageBody + 'static,
|
|
||||||
F: IntoServiceFactory<S, Request>,
|
F: IntoServiceFactory<S, Request>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
|
||||||
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
let cfg = ServiceConfig::new(
|
let cfg = ServiceConfig::new(
|
||||||
self.keep_alive,
|
self.keep_alive,
|
||||||
@@ -228,12 +222,12 @@ where
|
|||||||
/// Finish service configuration and create `HttpService` instance.
|
/// Finish service configuration and create `HttpService` instance.
|
||||||
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
|
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
B: MessageBody + 'static,
|
|
||||||
F: IntoServiceFactory<S, Request>,
|
F: IntoServiceFactory<S, Request>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
|
||||||
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
let cfg = ServiceConfig::new(
|
let cfg = ServiceConfig::new(
|
||||||
self.keep_alive,
|
self.keep_alive,
|
||||||
|
|||||||
@@ -1,267 +0,0 @@
|
|||||||
use std::future::Future;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
use std::{fmt, io, time};
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
use crate::body::MessageBody;
|
|
||||||
use crate::h1::ClientCodec;
|
|
||||||
use crate::message::{RequestHeadType, ResponseHead};
|
|
||||||
use crate::payload::Payload;
|
|
||||||
|
|
||||||
use super::error::SendRequestError;
|
|
||||||
use super::pool::{Acquired, Protocol};
|
|
||||||
use super::{h1proto, h2proto};
|
|
||||||
|
|
||||||
pub(crate) enum ConnectionType<Io> {
|
|
||||||
H1(Io),
|
|
||||||
H2(SendRequest<Bytes>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Connection {
|
|
||||||
type Io: AsyncRead + AsyncWrite + Unpin;
|
|
||||||
type Future: Future<Output = Result<(ResponseHead, Payload), SendRequestError>>;
|
|
||||||
|
|
||||||
fn protocol(&self) -> Protocol;
|
|
||||||
|
|
||||||
/// Send request and body
|
|
||||||
fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
|
|
||||||
self,
|
|
||||||
head: H,
|
|
||||||
body: B,
|
|
||||||
) -> Self::Future;
|
|
||||||
|
|
||||||
type TunnelFuture: Future<
|
|
||||||
Output = Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
/// Send request, returns Response and Framed
|
|
||||||
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
|
|
||||||
/// Close connection
|
|
||||||
fn close(self: Pin<&mut Self>);
|
|
||||||
|
|
||||||
/// Release connection to the connection pool
|
|
||||||
fn release(self: Pin<&mut Self>);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
/// HTTP client connection
|
|
||||||
pub struct IoConnection<T> {
|
|
||||||
io: Option<ConnectionType<T>>,
|
|
||||||
created: time::Instant,
|
|
||||||
pool: Option<Acquired<T>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> fmt::Debug for IoConnection<T>
|
|
||||||
where
|
|
||||||
T: fmt::Debug,
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self.io {
|
|
||||||
Some(ConnectionType::H1(ref io)) => write!(f, "H1Connection({:?})", io),
|
|
||||||
Some(ConnectionType::H2(_)) => write!(f, "H2Connection"),
|
|
||||||
None => write!(f, "Connection(Empty)"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsyncRead + AsyncWrite + Unpin> IoConnection<T> {
|
|
||||||
pub(crate) fn new(
|
|
||||||
io: ConnectionType<T>,
|
|
||||||
created: time::Instant,
|
|
||||||
pool: Option<Acquired<T>>,
|
|
||||||
) -> Self {
|
|
||||||
IoConnection {
|
|
||||||
pool,
|
|
||||||
created,
|
|
||||||
io: Some(io),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn into_inner(self) -> (ConnectionType<T>, time::Instant) {
|
|
||||||
(self.io.unwrap(), self.created)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Connection for IoConnection<T>
|
|
||||||
where
|
|
||||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
type Io = T;
|
|
||||||
type Future =
|
|
||||||
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
|
|
||||||
|
|
||||||
fn protocol(&self) -> Protocol {
|
|
||||||
match self.io {
|
|
||||||
Some(ConnectionType::H1(_)) => Protocol::Http1,
|
|
||||||
Some(ConnectionType::H2(_)) => Protocol::Http2,
|
|
||||||
None => Protocol::Http1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
|
|
||||||
mut self,
|
|
||||||
head: H,
|
|
||||||
body: B,
|
|
||||||
) -> Self::Future {
|
|
||||||
match self.io.take().unwrap() {
|
|
||||||
ConnectionType::H1(io) => {
|
|
||||||
h1proto::send_request(io, head.into(), body, self.created, self.pool)
|
|
||||||
.boxed_local()
|
|
||||||
}
|
|
||||||
ConnectionType::H2(io) => {
|
|
||||||
h2proto::send_request(io, head.into(), body, self.created, self.pool)
|
|
||||||
.boxed_local()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TunnelFuture = Either<
|
|
||||||
LocalBoxFuture<
|
|
||||||
'static,
|
|
||||||
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
|
||||||
>,
|
|
||||||
Ready<Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
/// Send request, returns Response and Framed
|
|
||||||
fn open_tunnel<H: Into<RequestHeadType>>(mut self, head: H) -> Self::TunnelFuture {
|
|
||||||
match self.io.take().unwrap() {
|
|
||||||
ConnectionType::H1(io) => {
|
|
||||||
Either::Left(h1proto::open_tunnel(io, head.into()).boxed_local())
|
|
||||||
}
|
|
||||||
ConnectionType::H2(io) => {
|
|
||||||
if let Some(mut pool) = self.pool.take() {
|
|
||||||
pool.release(IoConnection::new(
|
|
||||||
ConnectionType::H2(io),
|
|
||||||
self.created,
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Either::Right(err(SendRequestError::TunnelNotSupported))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) enum EitherConnection<A, B> {
|
|
||||||
A(IoConnection<A>),
|
|
||||||
B(IoConnection<B>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A, B> Connection for EitherConnection<A, B>
|
|
||||||
where
|
|
||||||
A: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
B: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
type Io = EitherIo<A, B>;
|
|
||||||
type Future =
|
|
||||||
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
|
|
||||||
|
|
||||||
fn protocol(&self) -> Protocol {
|
|
||||||
match self {
|
|
||||||
EitherConnection::A(con) => con.protocol(),
|
|
||||||
EitherConnection::B(con) => con.protocol(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_request<RB: MessageBody + 'static, H: Into<RequestHeadType>>(
|
|
||||||
self,
|
|
||||||
head: H,
|
|
||||||
body: RB,
|
|
||||||
) -> Self::Future {
|
|
||||||
match self {
|
|
||||||
EitherConnection::A(con) => con.send_request(head, body),
|
|
||||||
EitherConnection::B(con) => con.send_request(head, body),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TunnelFuture = LocalBoxFuture<
|
|
||||||
'static,
|
|
||||||
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
/// Send request, returns Response and Framed
|
|
||||||
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture {
|
|
||||||
match self {
|
|
||||||
EitherConnection::A(con) => con
|
|
||||||
.open_tunnel(head)
|
|
||||||
.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.into_map_io(EitherIo::B)))
|
|
||||||
})
|
|
||||||
.boxed_local(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project(project = EitherIoProj)]
|
|
||||||
pub enum EitherIo<A, B> {
|
|
||||||
A(#[pin] A),
|
|
||||||
B(#[pin] B),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A, B> AsyncRead for EitherIo<A, B>
|
|
||||||
where
|
|
||||||
A: AsyncRead,
|
|
||||||
B: AsyncRead,
|
|
||||||
{
|
|
||||||
fn poll_read(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A, B> AsyncWrite for EitherIo<A, B>
|
|
||||||
where
|
|
||||||
A: AsyncWrite,
|
|
||||||
B: AsyncWrite,
|
|
||||||
{
|
|
||||||
fn poll_write(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
buf: &[u8],
|
|
||||||
) -> Poll<io::Result<usize>> {
|
|
||||||
match self.project() {
|
|
||||||
EitherIoProj::A(val) => val.poll_write(cx, buf),
|
|
||||||
EitherIoProj::B(val) => val.poll_write(cx, buf),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
||||||
match self.project() {
|
|
||||||
EitherIoProj::A(val) => val.poll_flush(cx),
|
|
||||||
EitherIoProj::B(val) => val.poll_flush(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_shutdown(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<io::Result<()>> {
|
|
||||||
match self.project() {
|
|
||||||
EitherIoProj::A(val) => val.poll_shutdown(cx),
|
|
||||||
EitherIoProj::B(val) => val.poll_shutdown(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,534 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite};
|
|
||||||
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_utils::timeout::{TimeoutError, TimeoutService};
|
|
||||||
use http::Uri;
|
|
||||||
|
|
||||||
use super::config::ConnectorConfig;
|
|
||||||
use super::connection::Connection;
|
|
||||||
use super::error::ConnectError;
|
|
||||||
use super::pool::{ConnectionPool, Protocol};
|
|
||||||
use super::Connect;
|
|
||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
|
||||||
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
|
|
||||||
|
|
||||||
#[cfg(feature = "rustls")]
|
|
||||||
use actix_tls::connect::ssl::rustls::ClientConfig;
|
|
||||||
#[cfg(feature = "rustls")]
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
|
||||||
enum SslConnector {
|
|
||||||
#[cfg(feature = "openssl")]
|
|
||||||
Openssl(OpensslConnector),
|
|
||||||
#[cfg(feature = "rustls")]
|
|
||||||
Rustls(Arc<ClientConfig>),
|
|
||||||
}
|
|
||||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
|
||||||
type SslConnector = ();
|
|
||||||
|
|
||||||
/// Manages http client network connectivity
|
|
||||||
/// The `Connector` type uses a builder-like combinator pattern for service
|
|
||||||
/// construction that finishes by calling the `.finish()` method.
|
|
||||||
///
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// use std::time::Duration;
|
|
||||||
/// use actix_http::client::Connector;
|
|
||||||
///
|
|
||||||
/// let connector = Connector::new()
|
|
||||||
/// .timeout(Duration::from_secs(5))
|
|
||||||
/// .finish();
|
|
||||||
/// ```
|
|
||||||
pub struct Connector<T, U> {
|
|
||||||
connector: T,
|
|
||||||
config: ConnectorConfig,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
ssl: SslConnector,
|
|
||||||
_phantom: PhantomData<U>,
|
|
||||||
}
|
|
||||||
|
|
||||||
trait Io: AsyncRead + AsyncWrite + Unpin {}
|
|
||||||
impl<T: AsyncRead + AsyncWrite + Unpin> Io for T {}
|
|
||||||
|
|
||||||
impl Connector<(), ()> {
|
|
||||||
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
|
|
||||||
pub fn new() -> Connector<
|
|
||||||
impl Service<
|
|
||||||
TcpConnect<Uri>,
|
|
||||||
Response = TcpConnection<Uri, TcpStream>,
|
|
||||||
Error = actix_tls::connect::ConnectError,
|
|
||||||
> + Clone,
|
|
||||||
TcpStream,
|
|
||||||
> {
|
|
||||||
Connector {
|
|
||||||
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
|
|
||||||
connector: default_connector(),
|
|
||||||
config: ConnectorConfig::default(),
|
|
||||||
_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_tls::connect::ssl::openssl::SslMethod;
|
|
||||||
use bytes::{BufMut, BytesMut};
|
|
||||||
|
|
||||||
let mut alpn = BytesMut::with_capacity(20);
|
|
||||||
for proto in protocols.iter() {
|
|
||||||
alpn.put_u8(proto.len() as u8);
|
|
||||||
alpn.put(proto.as_slice());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap();
|
|
||||||
let _ = ssl
|
|
||||||
.set_alpn_protos(&alpn)
|
|
||||||
.map_err(|e| error!("Can not set alpn protocol: {:?}", e));
|
|
||||||
SslConnector::Openssl(ssl.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build Ssl connector with rustls, based on supplied alpn protocols
|
|
||||||
#[cfg(all(not(feature = "openssl"), feature = "rustls"))]
|
|
||||||
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
|
|
||||||
let mut config = ClientConfig::new();
|
|
||||||
config.set_protocols(&protocols);
|
|
||||||
config
|
|
||||||
.root_store
|
|
||||||
.add_server_trust_anchors(&actix_tls::accept::rustls::TLS_SERVER_ROOTS);
|
|
||||||
SslConnector::Rustls(Arc::new(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ssl turned off, provides empty ssl connector
|
|
||||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
|
||||||
fn build_ssl(_: Vec<Vec<u8>>) -> SslConnector {}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, U> Connector<T, U> {
|
|
||||||
/// Use custom connector.
|
|
||||||
pub fn connector<T1, U1>(self, connector: T1) -> Connector<T1, U1>
|
|
||||||
where
|
|
||||||
U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug,
|
|
||||||
T1: Service<
|
|
||||||
TcpConnect<Uri>,
|
|
||||||
Response = TcpConnection<Uri, U1>,
|
|
||||||
Error = actix_tls::connect::ConnectError,
|
|
||||||
> + Clone,
|
|
||||||
{
|
|
||||||
Connector {
|
|
||||||
connector,
|
|
||||||
config: self.config,
|
|
||||||
ssl: self.ssl,
|
|
||||||
_phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, U> Connector<T, U>
|
|
||||||
where
|
|
||||||
U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static,
|
|
||||||
T: Service<
|
|
||||||
TcpConnect<Uri>,
|
|
||||||
Response = TcpConnection<Uri, U>,
|
|
||||||
Error = actix_tls::connect::ConnectError,
|
|
||||||
> + Clone
|
|
||||||
+ 'static,
|
|
||||||
{
|
|
||||||
/// Connection timeout, i.e. max time to connect to remote host including dns name resolution.
|
|
||||||
/// Set to 1 second by default.
|
|
||||||
pub fn timeout(mut self, timeout: Duration) -> Self {
|
|
||||||
self.config.timeout = timeout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
|
||||||
/// Use custom `SslConnector` instance.
|
|
||||||
pub fn ssl(mut self, connector: OpensslConnector) -> Self {
|
|
||||||
self.ssl = SslConnector::Openssl(connector);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "rustls")]
|
|
||||||
pub fn rustls(mut self, connector: Arc<ClientConfig>) -> Self {
|
|
||||||
self.ssl = SslConnector::Rustls(connector);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maximum supported http major version
|
|
||||||
/// Supported versions http/1.1, http/2
|
|
||||||
pub fn max_http_version(mut self, val: http::Version) -> Self {
|
|
||||||
let versions = match val {
|
|
||||||
http::Version::HTTP_11 => vec![b"http/1.1".to_vec()],
|
|
||||||
http::Version::HTTP_2 => vec![b"h2".to_vec(), b"http/1.1".to_vec()],
|
|
||||||
_ => {
|
|
||||||
unimplemented!("actix-http:client: supported versions http/1.1, http/2")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.ssl = Connector::build_ssl(versions);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indicates the initial window size (in octets) for
|
|
||||||
/// HTTP2 stream-level flow control for received data.
|
|
||||||
///
|
|
||||||
/// The default value is 65,535 and is good for APIs, but not for big objects.
|
|
||||||
pub fn initial_window_size(mut self, size: u32) -> Self {
|
|
||||||
self.config.stream_window_size = size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indicates the initial window size (in octets) for
|
|
||||||
/// HTTP2 connection-level flow control for received data.
|
|
||||||
///
|
|
||||||
/// The default value is 65,535 and is good for APIs, but not for big objects.
|
|
||||||
pub fn initial_connection_window_size(mut self, size: u32) -> Self {
|
|
||||||
self.config.conn_window_size = size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set total number of simultaneous connections per type of scheme.
|
|
||||||
///
|
|
||||||
/// If limit is 0, the connector has no limit.
|
|
||||||
/// The default limit size is 100.
|
|
||||||
pub fn limit(mut self, limit: usize) -> Self {
|
|
||||||
self.config.limit = limit;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set keep-alive period for opened connection.
|
|
||||||
///
|
|
||||||
/// Keep-alive period is the period between connection usage. If
|
|
||||||
/// the delay between repeated usages of the same connection
|
|
||||||
/// exceeds this period, the connection is closed.
|
|
||||||
/// Default keep-alive period is 15 seconds.
|
|
||||||
pub fn conn_keep_alive(mut self, dur: Duration) -> Self {
|
|
||||||
self.config.conn_keep_alive = dur;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set max lifetime period for connection.
|
|
||||||
///
|
|
||||||
/// Connection lifetime is max lifetime of any opened connection
|
|
||||||
/// until it is closed regardless of keep-alive period.
|
|
||||||
/// Default lifetime period is 75 seconds.
|
|
||||||
pub fn conn_lifetime(mut self, dur: Duration) -> Self {
|
|
||||||
self.config.conn_lifetime = dur;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set server connection disconnect timeout in milliseconds.
|
|
||||||
///
|
|
||||||
/// Defines a timeout for disconnect connection. If a disconnect procedure does not complete
|
|
||||||
/// within this time, the socket get dropped. This timeout affects only secure connections.
|
|
||||||
///
|
|
||||||
/// To disable timeout set value to 0.
|
|
||||||
///
|
|
||||||
/// By default disconnect timeout is set to 3000 milliseconds.
|
|
||||||
pub fn disconnect_timeout(mut self, dur: Duration) -> Self {
|
|
||||||
self.config.disconnect_timeout = Some(dur);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finish configuration process and create connector service.
|
|
||||||
/// The Connector builder always concludes by calling `finish()` last in
|
|
||||||
/// its combinator chain.
|
|
||||||
pub fn finish(
|
|
||||||
self,
|
|
||||||
) -> impl Service<Connect, Response = impl Connection, Error = ConnectError> + Clone
|
|
||||||
{
|
|
||||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
|
||||||
{
|
|
||||||
let connector = TimeoutService::new(
|
|
||||||
self.config.timeout,
|
|
||||||
apply_fn(self.connector, |msg: Connect, srv| {
|
|
||||||
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
|
|
||||||
})
|
|
||||||
.map_err(ConnectError::from)
|
|
||||||
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
|
|
||||||
)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
TimeoutError::Service(e) => e,
|
|
||||||
TimeoutError::Timeout => ConnectError::Timeout,
|
|
||||||
});
|
|
||||||
|
|
||||||
connect_impl::InnerConnector {
|
|
||||||
tcp_pool: ConnectionPool::new(
|
|
||||||
connector,
|
|
||||||
self.config.no_disconnect_timeout(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
|
||||||
{
|
|
||||||
const H2: &[u8] = b"h2";
|
|
||||||
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,
|
|
||||||
pipeline(
|
|
||||||
apply_fn(self.connector.clone(), |msg: Connect, srv| {
|
|
||||||
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
|
|
||||||
})
|
|
||||||
.map_err(ConnectError::from),
|
|
||||||
)
|
|
||||||
.and_then(match self.ssl {
|
|
||||||
#[cfg(feature = "openssl")]
|
|
||||||
SslConnector::Openssl(ssl) => service(
|
|
||||||
OpensslConnector::service(ssl)
|
|
||||||
.map(|stream| {
|
|
||||||
let sock = stream.into_parts().0;
|
|
||||||
let h2 = sock
|
|
||||||
.ssl()
|
|
||||||
.selected_alpn_protocol()
|
|
||||||
.map(|protos| protos.windows(2).any(|w| w == H2))
|
|
||||||
.unwrap_or(false);
|
|
||||||
if h2 {
|
|
||||||
(Box::new(sock) as Box<dyn Io>, Protocol::Http2)
|
|
||||||
} else {
|
|
||||||
(Box::new(sock) as Box<dyn Io>, Protocol::Http1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map_err(ConnectError::from),
|
|
||||||
),
|
|
||||||
#[cfg(feature = "rustls")]
|
|
||||||
SslConnector::Rustls(ssl) => service(
|
|
||||||
RustlsConnector::service(ssl)
|
|
||||||
.map_err(ConnectError::from)
|
|
||||||
.map(|stream| {
|
|
||||||
let sock = stream.into_parts().0;
|
|
||||||
let h2 = sock
|
|
||||||
.get_ref()
|
|
||||||
.1
|
|
||||||
.get_alpn_protocol()
|
|
||||||
.map(|protos| protos.windows(2).any(|w| w == H2))
|
|
||||||
.unwrap_or(false);
|
|
||||||
if h2 {
|
|
||||||
(Box::new(sock) as Box<dyn Io>, Protocol::Http2)
|
|
||||||
} else {
|
|
||||||
(Box::new(sock) as Box<dyn Io>, Protocol::Http1)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
TimeoutError::Service(e) => e,
|
|
||||||
TimeoutError::Timeout => ConnectError::Timeout,
|
|
||||||
});
|
|
||||||
|
|
||||||
let tcp_service = TimeoutService::new(
|
|
||||||
self.config.timeout,
|
|
||||||
apply_fn(self.connector, |msg: Connect, srv| {
|
|
||||||
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
|
|
||||||
})
|
|
||||||
.map_err(ConnectError::from)
|
|
||||||
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
|
|
||||||
)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
TimeoutError::Service(e) => e,
|
|
||||||
TimeoutError::Timeout => ConnectError::Timeout,
|
|
||||||
});
|
|
||||||
|
|
||||||
connect_impl::InnerConnector {
|
|
||||||
tcp_pool: ConnectionPool::new(
|
|
||||||
tcp_service,
|
|
||||||
self.config.no_disconnect_timeout(),
|
|
||||||
),
|
|
||||||
ssl_pool: ConnectionPool::new(ssl_service, self.config),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
|
||||||
mod connect_impl {
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
|
|
||||||
use futures_util::future::{err, Either, Ready};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::client::connection::IoConnection;
|
|
||||||
|
|
||||||
pub(crate) struct InnerConnector<T, Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
|
||||||
{
|
|
||||||
pub(crate) tcp_pool: ConnectionPool<T, Io>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, Io> Clone for InnerConnector<T, Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
InnerConnector {
|
|
||||||
tcp_pool: self.tcp_pool.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, Io> Service<Connect> for InnerConnector<T, Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
|
||||||
{
|
|
||||||
type Response = IoConnection<Io>;
|
|
||||||
type Error = ConnectError;
|
|
||||||
type Future = Either<
|
|
||||||
<ConnectionPool<T, Io> as Service<Connect>>::Future,
|
|
||||||
Ready<Result<IoConnection<Io>, ConnectError>>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
||||||
self.tcp_pool.poll_ready(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(&mut self, req: Connect) -> Self::Future {
|
|
||||||
match req.uri.scheme_str() {
|
|
||||||
Some("https") | Some("wss") => {
|
|
||||||
Either::Right(err(ConnectError::SslIsNotSupported))
|
|
||||||
}
|
|
||||||
_ => Either::Left(self.tcp_pool.call(req)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
|
||||||
mod connect_impl {
|
|
||||||
use std::future::Future;
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
|
|
||||||
use futures_core::ready;
|
|
||||||
use futures_util::future::Either;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::client::connection::EitherConnection;
|
|
||||||
|
|
||||||
pub(crate) struct InnerConnector<T1, T2, Io1, Io2>
|
|
||||||
where
|
|
||||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
|
|
||||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
|
|
||||||
{
|
|
||||||
pub(crate) tcp_pool: ConnectionPool<T1, Io1>,
|
|
||||||
pub(crate) ssl_pool: ConnectionPool<T2, Io2>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T1, T2, Io1, Io2> Clone for InnerConnector<T1, T2, Io1, Io2>
|
|
||||||
where
|
|
||||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
|
||||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
InnerConnector {
|
|
||||||
tcp_pool: self.tcp_pool.clone(),
|
|
||||||
ssl_pool: self.ssl_pool.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
|
||||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
|
||||||
{
|
|
||||||
type Response = EitherConnection<Io1, Io2>;
|
|
||||||
type Error = ConnectError;
|
|
||||||
type Future = Either<
|
|
||||||
InnerConnectorResponseA<T1, Io1, Io2>,
|
|
||||||
InnerConnectorResponseB<T2, Io1, Io2>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
||||||
self.tcp_pool.poll_ready(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(&mut self, req: Connect) -> Self::Future {
|
|
||||||
match req.uri.scheme_str() {
|
|
||||||
Some("https") | Some("wss") => Either::Right(InnerConnectorResponseB {
|
|
||||||
fut: self.ssl_pool.call(req),
|
|
||||||
_phantom: PhantomData,
|
|
||||||
}),
|
|
||||||
_ => Either::Left(InnerConnectorResponseA {
|
|
||||||
fut: self.tcp_pool.call(req),
|
|
||||||
_phantom: PhantomData,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project::pin_project]
|
|
||||||
pub(crate) struct InnerConnectorResponseA<T, Io1, Io2>
|
|
||||||
where
|
|
||||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
|
||||||
{
|
|
||||||
#[pin]
|
|
||||||
fut: <ConnectionPool<T, Io1> as Service<Connect>>::Future,
|
|
||||||
_phantom: PhantomData<Io2>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, Io1, Io2> Future for InnerConnectorResponseA<T, Io1, Io2>
|
|
||||||
where
|
|
||||||
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
|
||||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
type Output = Result<EitherConnection<Io1, Io2>, ConnectError>;
|
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
||||||
Poll::Ready(
|
|
||||||
ready!(Pin::new(&mut self.get_mut().fut).poll(cx))
|
|
||||||
.map(EitherConnection::A),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project::pin_project]
|
|
||||||
pub(crate) struct InnerConnectorResponseB<T, Io1, Io2>
|
|
||||||
where
|
|
||||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
|
||||||
{
|
|
||||||
#[pin]
|
|
||||||
fut: <ConnectionPool<T, Io2> as Service<Connect>>::Future,
|
|
||||||
_phantom: PhantomData<Io1>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, Io1, Io2> Future for InnerConnectorResponseB<T, Io1, Io2>
|
|
||||||
where
|
|
||||||
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
|
||||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
type Output = Result<EitherConnection<Io1, Io2>, ConnectError>;
|
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
||||||
Poll::Ready(
|
|
||||||
ready!(Pin::new(&mut self.get_mut().fut).poll(cx))
|
|
||||||
.map(EitherConnection::B),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
use std::io::Write;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
use std::{io, time};
|
|
||||||
|
|
||||||
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;
|
|
||||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
|
||||||
|
|
||||||
use crate::error::PayloadError;
|
|
||||||
use crate::h1;
|
|
||||||
use crate::header::HeaderMap;
|
|
||||||
use crate::http::header::{IntoHeaderValue, HOST};
|
|
||||||
use crate::message::{RequestHeadType, ResponseHead};
|
|
||||||
use crate::payload::{Payload, PayloadStream};
|
|
||||||
|
|
||||||
use super::connection::{ConnectionLifetime, ConnectionType, IoConnection};
|
|
||||||
use super::error::{ConnectError, SendRequestError};
|
|
||||||
use super::pool::Acquired;
|
|
||||||
use crate::body::{BodySize, MessageBody};
|
|
||||||
|
|
||||||
pub(crate) async fn send_request<T, B>(
|
|
||||||
io: T,
|
|
||||||
mut head: RequestHeadType,
|
|
||||||
body: B,
|
|
||||||
created: time::Instant,
|
|
||||||
pool: Option<Acquired<T>>,
|
|
||||||
) -> Result<(ResponseHead, Payload), SendRequestError>
|
|
||||||
where
|
|
||||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
B: MessageBody,
|
|
||||||
{
|
|
||||||
// set request host header
|
|
||||||
if !head.as_ref().headers.contains_key(HOST)
|
|
||||||
&& !head.extra_headers().iter().any(|h| h.contains_key(HOST))
|
|
||||||
{
|
|
||||||
if let Some(host) = head.as_ref().uri.host() {
|
|
||||||
let mut wrt = BytesMut::with_capacity(host.len() + 5).writer();
|
|
||||||
|
|
||||||
let _ = match head.as_ref().uri.port_u16() {
|
|
||||||
None | Some(80) | Some(443) => write!(wrt, "{}", host),
|
|
||||||
Some(port) => write!(wrt, "{}:{}", host, port),
|
|
||||||
};
|
|
||||||
|
|
||||||
match wrt.get_mut().split().freeze().try_into() {
|
|
||||||
Ok(value) => match head {
|
|
||||||
RequestHeadType::Owned(ref mut head) => {
|
|
||||||
head.headers.insert(HOST, value)
|
|
||||||
}
|
|
||||||
RequestHeadType::Rc(_, ref mut extra_headers) => {
|
|
||||||
let headers = extra_headers.get_or_insert(HeaderMap::new());
|
|
||||||
headers.insert(HOST, value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => log::error!("Can not set HOST header {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let io = H1Connection {
|
|
||||||
created,
|
|
||||||
pool,
|
|
||||||
io: Some(io),
|
|
||||||
};
|
|
||||||
|
|
||||||
// create Framed and send request
|
|
||||||
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, Pin::new(&mut framed_inner)).await?,
|
|
||||||
};
|
|
||||||
|
|
||||||
// read response and init read body
|
|
||||||
let res = Pin::new(&mut framed_inner).into_future().await;
|
|
||||||
let (head, framed) = if let (Some(result), framed) = res {
|
|
||||||
let item = result.map_err(SendRequestError::from)?;
|
|
||||||
(item, framed)
|
|
||||||
} else {
|
|
||||||
return Err(SendRequestError::from(ConnectError::Disconnected));
|
|
||||||
};
|
|
||||||
|
|
||||||
match framed.codec_ref().message_type() {
|
|
||||||
h1::MessageType::None => {
|
|
||||||
let force_close = !framed.codec_ref().keepalive();
|
|
||||||
release_connection(framed, force_close);
|
|
||||||
Ok((head, Payload::None))
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let pl: PayloadStream = PlStream::new(framed_inner).boxed_local();
|
|
||||||
Ok((head, pl.into()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn open_tunnel<T>(
|
|
||||||
io: T,
|
|
||||||
head: RequestHeadType,
|
|
||||||
) -> Result<(ResponseHead, Framed<T, h1::ClientCodec>), SendRequestError>
|
|
||||||
where
|
|
||||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
// create Framed and send request
|
|
||||||
let mut framed = Framed::new(io, h1::ClientCodec::default());
|
|
||||||
framed.send((head, BodySize::None).into()).await?;
|
|
||||||
|
|
||||||
// read response
|
|
||||||
if let (Some(result), framed) = framed.into_future().await {
|
|
||||||
let head = result.map_err(SendRequestError::from)?;
|
|
||||||
Ok((head, framed))
|
|
||||||
} else {
|
|
||||||
Err(SendRequestError::from(ConnectError::Disconnected))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// send request body to the peer
|
|
||||||
pub(crate) async fn send_body<T, B>(
|
|
||||||
body: B,
|
|
||||||
mut framed: Pin<&mut Framed<T, h1::ClientCodec>>,
|
|
||||||
) -> Result<(), SendRequestError>
|
|
||||||
where
|
|
||||||
T: ConnectionLifetime + Unpin,
|
|
||||||
B: MessageBody,
|
|
||||||
{
|
|
||||||
pin_mut!(body);
|
|
||||||
|
|
||||||
let mut eof = false;
|
|
||||||
while !eof {
|
|
||||||
while !eof && !framed.as_ref().is_write_buf_full() {
|
|
||||||
match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
|
||||||
Some(result) => {
|
|
||||||
framed.as_mut().write(h1::Message::Chunk(Some(result?)))?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
eof = true;
|
|
||||||
framed.as_mut().write(h1::Message::Chunk(None))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.as_ref().is_write_buf_full() {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
} else {
|
|
||||||
Poll::Pending
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.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>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> ConnectionLifetime for H1Connection<T>
|
|
||||||
where
|
|
||||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
/// Close connection
|
|
||||||
fn close(mut self: Pin<&mut Self>) {
|
|
||||||
if let Some(mut pool) = self.pool.take() {
|
|
||||||
if let Some(io) = self.io.take() {
|
|
||||||
pool.close(IoConnection::new(
|
|
||||||
ConnectionType::H1(io),
|
|
||||||
self.created,
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Release this connection to the connection pool
|
|
||||||
fn release(mut self: Pin<&mut Self>) {
|
|
||||||
if let Some(mut pool) = self.pool.take() {
|
|
||||||
if let Some(io) = self.io.take() {
|
|
||||||
pool.release(IoConnection::new(
|
|
||||||
ConnectionType::H1(io),
|
|
||||||
self.created,
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncRead for H1Connection<T> {
|
|
||||||
fn poll_read(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
buf: &mut ReadBuf<'_>,
|
|
||||||
) -> Poll<io::Result<()>> {
|
|
||||||
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncWrite for H1Connection<T> {
|
|
||||||
fn poll_write(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
buf: &[u8],
|
|
||||||
) -> Poll<io::Result<usize>> {
|
|
||||||
Pin::new(&mut self.io.as_mut().unwrap()).poll_write(cx, buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_flush(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<io::Result<()>> {
|
|
||||||
Pin::new(self.io.as_mut().unwrap()).poll_flush(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_shutdown(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Result<(), io::Error>> {
|
|
||||||
Pin::new(self.io.as_mut().unwrap()).poll_shutdown(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project::pin_project]
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
|
|
||||||
type Item = Result<Bytes, PayloadError>;
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Self::Item>> {
|
|
||||||
let mut this = self.project();
|
|
||||||
|
|
||||||
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.as_mut().as_pin_mut().unwrap();
|
|
||||||
let force_close = !framed.codec_ref().keepalive();
|
|
||||||
release_connection(framed, force_close);
|
|
||||||
Poll::Ready(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Poll::Ready(None) => Poll::Ready(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release_connection<T, U>(framed: Pin<&mut Framed<T, U>>, force_close: bool)
|
|
||||||
where
|
|
||||||
T: ConnectionLifetime,
|
|
||||||
{
|
|
||||||
if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() {
|
|
||||||
framed.io_pin().release()
|
|
||||||
} else {
|
|
||||||
framed.io_pin().close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,645 +0,0 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
|
||||||
use actix_rt::time::{sleep, Sleep};
|
|
||||||
use actix_service::Service;
|
|
||||||
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};
|
|
||||||
use http::uri::Authority;
|
|
||||||
use indexmap::IndexSet;
|
|
||||||
use pin_project::pin_project;
|
|
||||||
use slab::Slab;
|
|
||||||
|
|
||||||
use super::config::ConnectorConfig;
|
|
||||||
use super::connection::{ConnectionType, IoConnection};
|
|
||||||
use super::error::ConnectError;
|
|
||||||
use super::h2proto::handshake;
|
|
||||||
use super::Connect;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
/// Protocol version
|
|
||||||
pub enum Protocol {
|
|
||||||
Http1,
|
|
||||||
Http2,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
|
|
||||||
pub(crate) struct Key {
|
|
||||||
authority: Authority,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Authority> for Key {
|
|
||||||
fn from(authority: Authority) -> Key {
|
|
||||||
Key { authority }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connections pool
|
|
||||||
pub(crate) struct ConnectionPool<T, Io: 'static>(Rc<RefCell<T>>, Rc<RefCell<Inner<Io>>>);
|
|
||||||
|
|
||||||
impl<T, Io> ConnectionPool<T, Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + '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));
|
|
||||||
let inner_rc = Rc::new(RefCell::new(Inner {
|
|
||||||
config,
|
|
||||||
acquired: 0,
|
|
||||||
waiters: Slab::new(),
|
|
||||||
waiters_queue: IndexSet::new(),
|
|
||||||
available: FxHashMap::default(),
|
|
||||||
waker: LocalWaker::new(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// start support future
|
|
||||||
actix_rt::spawn(ConnectorPoolSupport {
|
|
||||||
connector: Rc::clone(&connector_rc),
|
|
||||||
inner: Rc::clone(&inner_rc),
|
|
||||||
});
|
|
||||||
|
|
||||||
ConnectionPool(connector_rc, inner_rc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, Io> Clone for ConnectionPool<T, Io>
|
|
||||||
where
|
|
||||||
Io: 'static,
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
ConnectionPool(self.0.clone(), self.1.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
|
||||||
{
|
|
||||||
type Response = IoConnection<Io>;
|
|
||||||
type Error = ConnectError;
|
|
||||||
type Future = LocalBoxFuture<'static, Result<IoConnection<Io>, ConnectError>>;
|
|
||||||
|
|
||||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
|
||||||
self.0.poll_ready(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(&mut self, req: Connect) -> Self::Future {
|
|
||||||
let mut connector = self.0.clone();
|
|
||||||
let inner = self.1.clone();
|
|
||||||
|
|
||||||
let fut = async move {
|
|
||||||
let key = if let Some(authority) = req.uri.authority() {
|
|
||||||
authority.clone().into()
|
|
||||||
} else {
|
|
||||||
return Err(ConnectError::Unresolved);
|
|
||||||
};
|
|
||||||
|
|
||||||
// acquire connection
|
|
||||||
match poll_fn(|cx| Poll::Ready(inner.borrow_mut().acquire(&key, cx))).await {
|
|
||||||
Acquire::Acquired(io, created) => {
|
|
||||||
// use existing connection
|
|
||||||
Ok(IoConnection::new(
|
|
||||||
io,
|
|
||||||
created,
|
|
||||||
Some(Acquired(key, Some(inner))),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Acquire::Available => {
|
|
||||||
// open tcp connection
|
|
||||||
let (io, proto) = connector.call(req).await?;
|
|
||||||
|
|
||||||
let config = inner.borrow().config.clone();
|
|
||||||
|
|
||||||
let guard = OpenGuard::new(key, inner);
|
|
||||||
|
|
||||||
if proto == Protocol::Http1 {
|
|
||||||
Ok(IoConnection::new(
|
|
||||||
ConnectionType::H1(io),
|
|
||||||
Instant::now(),
|
|
||||||
Some(guard.consume()),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
let (snd, connection) = handshake(io, &config).await?;
|
|
||||||
actix_rt::spawn(connection.map(|_| ()));
|
|
||||||
Ok(IoConnection::new(
|
|
||||||
ConnectionType::H2(snd),
|
|
||||||
Instant::now(),
|
|
||||||
Some(guard.consume()),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// connection is not available, wait
|
|
||||||
let (rx, token) = inner.borrow_mut().wait_for(req);
|
|
||||||
|
|
||||||
let guard = WaiterGuard::new(key, token, inner);
|
|
||||||
let res = match rx.await {
|
|
||||||
Err(_) => Err(ConnectError::Disconnected),
|
|
||||||
Ok(res) => res,
|
|
||||||
};
|
|
||||||
guard.consume();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fut.boxed_local()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WaiterGuard<Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
key: Key,
|
|
||||||
token: usize,
|
|
||||||
inner: Option<Rc<RefCell<Inner<Io>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Io> WaiterGuard<Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
fn new(key: Key, token: usize, inner: Rc<RefCell<Inner<Io>>>) -> Self {
|
|
||||||
Self {
|
|
||||||
key,
|
|
||||||
token,
|
|
||||||
inner: Some(inner),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume(mut self) {
|
|
||||||
let _ = self.inner.take();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Io> Drop for WaiterGuard<Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Some(i) = self.inner.take() {
|
|
||||||
let mut inner = i.as_ref().borrow_mut();
|
|
||||||
inner.release_waiter(&self.key, self.token);
|
|
||||||
inner.check_availability();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OpenGuard<Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
key: Key,
|
|
||||||
inner: Option<Rc<RefCell<Inner<Io>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Io> OpenGuard<Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
fn new(key: Key, inner: Rc<RefCell<Inner<Io>>>) -> Self {
|
|
||||||
Self {
|
|
||||||
key,
|
|
||||||
inner: Some(inner),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume(mut self) -> Acquired<Io> {
|
|
||||||
Acquired(self.key.clone(), self.inner.take())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Io> Drop for OpenGuard<Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Some(i) = self.inner.take() {
|
|
||||||
let mut inner = i.as_ref().borrow_mut();
|
|
||||||
inner.release();
|
|
||||||
inner.check_availability();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Acquire<T> {
|
|
||||||
Acquired(ConnectionType<T>, Instant),
|
|
||||||
Available,
|
|
||||||
NotAvailable,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AvailableConnection<Io> {
|
|
||||||
io: ConnectionType<Io>,
|
|
||||||
used: Instant,
|
|
||||||
created: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Inner<Io> {
|
|
||||||
config: ConnectorConfig,
|
|
||||||
acquired: usize,
|
|
||||||
available: FxHashMap<Key, VecDeque<AvailableConnection<Io>>>,
|
|
||||||
waiters: Slab<
|
|
||||||
Option<(
|
|
||||||
Connect,
|
|
||||||
oneshot::Sender<Result<IoConnection<Io>, ConnectError>>,
|
|
||||||
)>,
|
|
||||||
>,
|
|
||||||
waiters_queue: IndexSet<(Key, usize)>,
|
|
||||||
waker: LocalWaker,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Io> Inner<Io> {
|
|
||||||
fn reserve(&mut self) {
|
|
||||||
self.acquired += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release(&mut self) {
|
|
||||||
self.acquired -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release_waiter(&mut self, key: &Key, token: usize) {
|
|
||||||
self.waiters.remove(token);
|
|
||||||
let _ = self.waiters_queue.shift_remove(&(key.clone(), token));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Io> Inner<Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
/// connection is not available, wait
|
|
||||||
fn wait_for(
|
|
||||||
&mut self,
|
|
||||||
connect: Connect,
|
|
||||||
) -> (
|
|
||||||
oneshot::Receiver<Result<IoConnection<Io>, ConnectError>>,
|
|
||||||
usize,
|
|
||||||
) {
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
|
|
||||||
let key: Key = connect.uri.authority().unwrap().clone().into();
|
|
||||||
let entry = self.waiters.vacant_entry();
|
|
||||||
let token = entry.key();
|
|
||||||
entry.insert(Some((connect, tx)));
|
|
||||||
assert!(self.waiters_queue.insert((key, token)));
|
|
||||||
|
|
||||||
(rx, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn acquire(&mut self, key: &Key, cx: &mut Context<'_>) -> Acquire<Io> {
|
|
||||||
// check limits
|
|
||||||
if self.config.limit > 0 && self.acquired >= self.config.limit {
|
|
||||||
return Acquire::NotAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.reserve();
|
|
||||||
|
|
||||||
// check if open connection is available
|
|
||||||
// cleanup stale connections at the same time
|
|
||||||
if let Some(ref mut connections) = self.available.get_mut(key) {
|
|
||||||
let now = Instant::now();
|
|
||||||
while let Some(conn) = connections.pop_back() {
|
|
||||||
// check if it still usable
|
|
||||||
if (now - conn.used) > self.config.conn_keep_alive
|
|
||||||
|| (now - conn.created) > self.config.conn_lifetime
|
|
||||||
{
|
|
||||||
if let Some(timeout) = self.config.disconnect_timeout {
|
|
||||||
if let ConnectionType::H1(io) = conn.io {
|
|
||||||
actix_rt::spawn(CloseConnection::new(io, timeout))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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 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(
|
|
||||||
io, timeout,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Acquire::Acquired(io, conn.created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Acquire::Available
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release_conn(&mut self, key: &Key, io: ConnectionType<Io>, created: Instant) {
|
|
||||||
self.acquired -= 1;
|
|
||||||
self.available
|
|
||||||
.entry(key.clone())
|
|
||||||
.or_insert_with(VecDeque::new)
|
|
||||||
.push_back(AvailableConnection {
|
|
||||||
io,
|
|
||||||
created,
|
|
||||||
used: Instant::now(),
|
|
||||||
});
|
|
||||||
self.check_availability();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release_close(&mut self, io: ConnectionType<Io>) {
|
|
||||||
self.acquired -= 1;
|
|
||||||
if let Some(timeout) = self.config.disconnect_timeout {
|
|
||||||
if let ConnectionType::H1(io) = io {
|
|
||||||
actix_rt::spawn(CloseConnection::new(io, timeout))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.check_availability();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_availability(&self) {
|
|
||||||
if !self.waiters_queue.is_empty() && self.acquired < self.config.limit {
|
|
||||||
self.waker.wake();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project::pin_project]
|
|
||||||
struct CloseConnection<T> {
|
|
||||||
io: T,
|
|
||||||
#[pin]
|
|
||||||
timeout: Sleep,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> CloseConnection<T>
|
|
||||||
where
|
|
||||||
T: AsyncWrite + Unpin,
|
|
||||||
{
|
|
||||||
fn new(io: T, timeout: Duration) -> Self {
|
|
||||||
CloseConnection {
|
|
||||||
io,
|
|
||||||
timeout: sleep(timeout),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Future for CloseConnection<T>
|
|
||||||
where
|
|
||||||
T: AsyncWrite + Unpin,
|
|
||||||
{
|
|
||||||
type Output = ();
|
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
|
||||||
let this = self.project();
|
|
||||||
|
|
||||||
match this.timeout.poll(cx) {
|
|
||||||
Poll::Ready(_) => Poll::Ready(()),
|
|
||||||
Poll::Pending => match Pin::new(this.io).poll_shutdown(cx) {
|
|
||||||
Poll::Ready(_) => Poll::Ready(()),
|
|
||||||
Poll::Pending => Poll::Pending,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project]
|
|
||||||
struct ConnectorPoolSupport<T, Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
connector: T,
|
|
||||||
inner: Rc<RefCell<Inner<Io>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, Io> Future for ConnectorPoolSupport<T, Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError>,
|
|
||||||
T::Future: 'static,
|
|
||||||
{
|
|
||||||
type Output = ();
|
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
||||||
let this = self.project();
|
|
||||||
|
|
||||||
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(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut inner = this.inner.borrow_mut();
|
|
||||||
inner.waker.register(cx.waker());
|
|
||||||
|
|
||||||
// check waiters
|
|
||||||
loop {
|
|
||||||
let (key, token) = {
|
|
||||||
if let Some((key, token)) = inner.waiters_queue.get_index(0) {
|
|
||||||
(key.clone(), *token)
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if inner.waiters.get(token).unwrap().is_none() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match inner.acquire(&key, cx) {
|
|
||||||
Acquire::NotAvailable => break,
|
|
||||||
Acquire::Acquired(io, created) => {
|
|
||||||
let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1;
|
|
||||||
if let Err(conn) = tx.send(Ok(IoConnection::new(
|
|
||||||
io,
|
|
||||||
created,
|
|
||||||
Some(Acquired(key.clone(), Some(this.inner.clone()))),
|
|
||||||
))) {
|
|
||||||
let (io, created) = conn.unwrap().into_inner();
|
|
||||||
inner.release_conn(&key, io, created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Acquire::Available => {
|
|
||||||
let (connect, tx) =
|
|
||||||
inner.waiters.get_mut(token).unwrap().take().unwrap();
|
|
||||||
OpenWaitingConnection::spawn(
|
|
||||||
key.clone(),
|
|
||||||
tx,
|
|
||||||
this.inner.clone(),
|
|
||||||
this.connector.call(connect),
|
|
||||||
inner.config.clone(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = inner.waiters_queue.swap_remove_index(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Poll::Pending
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project::pin_project(PinnedDrop)]
|
|
||||||
struct OpenWaitingConnection<F, Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
#[pin]
|
|
||||||
fut: F,
|
|
||||||
key: Key,
|
|
||||||
h2: Option<
|
|
||||||
LocalBoxFuture<
|
|
||||||
'static,
|
|
||||||
Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
rx: Option<oneshot::Sender<Result<IoConnection<Io>, ConnectError>>>,
|
|
||||||
inner: Option<Rc<RefCell<Inner<Io>>>>,
|
|
||||||
config: ConnectorConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F, Io> OpenWaitingConnection<F, Io>
|
|
||||||
where
|
|
||||||
F: Future<Output = Result<(Io, Protocol), ConnectError>> + 'static,
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
fn spawn(
|
|
||||||
key: Key,
|
|
||||||
rx: oneshot::Sender<Result<IoConnection<Io>, ConnectError>>,
|
|
||||||
inner: Rc<RefCell<Inner<Io>>>,
|
|
||||||
fut: F,
|
|
||||||
config: ConnectorConfig,
|
|
||||||
) {
|
|
||||||
actix_rt::spawn(OpenWaitingConnection {
|
|
||||||
key,
|
|
||||||
fut,
|
|
||||||
h2: None,
|
|
||||||
rx: Some(rx),
|
|
||||||
inner: Some(inner),
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project::pinned_drop]
|
|
||||||
impl<F, Io> PinnedDrop for OpenWaitingConnection<F, Io>
|
|
||||||
where
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
fn drop(self: Pin<&mut Self>) {
|
|
||||||
if let Some(inner) = self.project().inner.take() {
|
|
||||||
let mut inner = inner.as_ref().borrow_mut();
|
|
||||||
inner.release();
|
|
||||||
inner.check_availability();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F, Io> Future for OpenWaitingConnection<F, Io>
|
|
||||||
where
|
|
||||||
F: Future<Output = Result<(Io, Protocol), ConnectError>>,
|
|
||||||
Io: AsyncRead + AsyncWrite + Unpin,
|
|
||||||
{
|
|
||||||
type Output = ();
|
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
||||||
let this = self.as_mut().project();
|
|
||||||
|
|
||||||
if let Some(ref mut h2) = this.h2 {
|
|
||||||
return match Pin::new(h2).poll(cx) {
|
|
||||||
Poll::Ready(Ok((snd, connection))) => {
|
|
||||||
actix_rt::spawn(connection.map(|_| ()));
|
|
||||||
let rx = this.rx.take().unwrap();
|
|
||||||
let _ = rx.send(Ok(IoConnection::new(
|
|
||||||
ConnectionType::H2(snd),
|
|
||||||
Instant::now(),
|
|
||||||
Some(Acquired(this.key.clone(), this.inner.take())),
|
|
||||||
)));
|
|
||||||
Poll::Ready(())
|
|
||||||
}
|
|
||||||
Poll::Pending => Poll::Pending,
|
|
||||||
Poll::Ready(Err(err)) => {
|
|
||||||
let _ = this.inner.take();
|
|
||||||
if let Some(rx) = this.rx.take() {
|
|
||||||
let _ = rx.send(Err(ConnectError::H2(err)));
|
|
||||||
}
|
|
||||||
Poll::Ready(())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
match this.fut.poll(cx) {
|
|
||||||
Poll::Ready(Err(err)) => {
|
|
||||||
let _ = this.inner.take();
|
|
||||||
if let Some(rx) = this.rx.take() {
|
|
||||||
let _ = rx.send(Err(err));
|
|
||||||
}
|
|
||||||
Poll::Ready(())
|
|
||||||
}
|
|
||||||
Poll::Ready(Ok((io, proto))) => {
|
|
||||||
if proto == Protocol::Http1 {
|
|
||||||
let rx = this.rx.take().unwrap();
|
|
||||||
let _ = rx.send(Ok(IoConnection::new(
|
|
||||||
ConnectionType::H1(io),
|
|
||||||
Instant::now(),
|
|
||||||
Some(Acquired(this.key.clone(), this.inner.take())),
|
|
||||||
)));
|
|
||||||
Poll::Ready(())
|
|
||||||
} else {
|
|
||||||
*this.h2 = Some(handshake(io, this.config).boxed_local());
|
|
||||||
self.poll(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Poll::Pending => Poll::Pending,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Acquired<T>(Key, Option<Rc<RefCell<Inner<T>>>>);
|
|
||||||
|
|
||||||
impl<T> Acquired<T>
|
|
||||||
where
|
|
||||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
pub(crate) fn close(&mut self, conn: IoConnection<T>) {
|
|
||||||
if let Some(inner) = self.1.take() {
|
|
||||||
let (io, _) = conn.into_inner();
|
|
||||||
inner.as_ref().borrow_mut().release_close(io);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(crate) fn release(&mut self, conn: IoConnection<T>) {
|
|
||||||
if let Some(inner) = self.1.take() {
|
|
||||||
let (io, created) = conn.into_inner();
|
|
||||||
inner
|
|
||||||
.as_ref()
|
|
||||||
.borrow_mut()
|
|
||||||
.release_conn(&self.0, io, created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Drop for Acquired<T> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Some(inner) = self.1.take() {
|
|
||||||
inner.as_ref().borrow_mut().release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,29 @@
|
|||||||
use std::cell::Cell;
|
use std::{
|
||||||
use std::fmt::Write;
|
cell::Cell,
|
||||||
use std::rc::Rc;
|
fmt::{self, Write},
|
||||||
use std::time::Duration;
|
net,
|
||||||
use std::{fmt, net};
|
rc::Rc,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
use actix_rt::time::{sleep, sleep_until, Instant, Sleep};
|
use actix_rt::{
|
||||||
|
task::JoinHandle,
|
||||||
|
time::{interval, sleep_until, Instant, Sleep},
|
||||||
|
};
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use futures_util::{future, FutureExt};
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
/// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||||
const DATE_VALUE_LENGTH: usize = 29;
|
pub(crate) const DATE_VALUE_LENGTH: usize = 29;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
/// Server keep-alive setting
|
/// Server keep-alive setting
|
||||||
pub enum KeepAlive {
|
pub enum KeepAlive {
|
||||||
/// Keep alive in seconds
|
/// Keep alive in seconds
|
||||||
Timeout(usize),
|
Timeout(usize),
|
||||||
|
|
||||||
/// Rely on OS to shutdown tcp connection
|
/// Rely on OS to shutdown tcp connection
|
||||||
Os,
|
Os,
|
||||||
|
|
||||||
/// Disabled
|
/// Disabled
|
||||||
Disabled,
|
Disabled,
|
||||||
}
|
}
|
||||||
@@ -49,7 +54,7 @@ struct Inner {
|
|||||||
ka_enabled: bool,
|
ka_enabled: bool,
|
||||||
secure: bool,
|
secure: bool,
|
||||||
local_addr: Option<std::net::SocketAddr>,
|
local_addr: Option<std::net::SocketAddr>,
|
||||||
timer: DateService,
|
date_service: DateService,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for ServiceConfig {
|
impl Clone for ServiceConfig {
|
||||||
@@ -91,42 +96,42 @@ impl ServiceConfig {
|
|||||||
client_disconnect,
|
client_disconnect,
|
||||||
secure,
|
secure,
|
||||||
local_addr,
|
local_addr,
|
||||||
timer: DateService::new(),
|
date_service: DateService::new(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if connection is secure (HTTPS)
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Returns true if connection is secure(https)
|
|
||||||
pub fn secure(&self) -> bool {
|
pub fn secure(&self) -> bool {
|
||||||
self.0.secure
|
self.0.secure
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Returns the local address that this server is bound to.
|
/// Returns the local address that this server is bound to.
|
||||||
|
///
|
||||||
|
/// Returns `None` for connections via UDS (Unix Domain Socket).
|
||||||
|
#[inline]
|
||||||
pub fn local_addr(&self) -> Option<net::SocketAddr> {
|
pub fn local_addr(&self) -> Option<net::SocketAddr> {
|
||||||
self.0.local_addr
|
self.0.local_addr
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Keep alive duration if configured.
|
/// Keep alive duration if configured.
|
||||||
|
#[inline]
|
||||||
pub fn keep_alive(&self) -> Option<Duration> {
|
pub fn keep_alive(&self) -> Option<Duration> {
|
||||||
self.0.keep_alive
|
self.0.keep_alive
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Return state of connection keep-alive functionality
|
/// Return state of connection keep-alive functionality
|
||||||
|
#[inline]
|
||||||
pub fn keep_alive_enabled(&self) -> bool {
|
pub fn keep_alive_enabled(&self) -> bool {
|
||||||
self.0.ka_enabled
|
self.0.ka_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Client timeout for first request.
|
/// Client timeout for first request.
|
||||||
|
#[inline]
|
||||||
pub fn client_timer(&self) -> Option<Sleep> {
|
pub fn client_timer(&self) -> Option<Sleep> {
|
||||||
let delay_time = self.0.client_timeout;
|
let delay_time = self.0.client_timeout;
|
||||||
if delay_time != 0 {
|
if delay_time != 0 {
|
||||||
Some(sleep_until(
|
Some(sleep_until(self.now() + Duration::from_millis(delay_time)))
|
||||||
self.0.timer.now() + Duration::from_millis(delay_time),
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -136,7 +141,7 @@ impl ServiceConfig {
|
|||||||
pub fn client_timer_expire(&self) -> Option<Instant> {
|
pub fn client_timer_expire(&self) -> Option<Instant> {
|
||||||
let delay = self.0.client_timeout;
|
let delay = self.0.client_timeout;
|
||||||
if delay != 0 {
|
if delay != 0 {
|
||||||
Some(self.0.timer.now() + Duration::from_millis(delay))
|
Some(self.now() + Duration::from_millis(delay))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -146,34 +151,26 @@ impl ServiceConfig {
|
|||||||
pub fn client_disconnect_timer(&self) -> Option<Instant> {
|
pub fn client_disconnect_timer(&self) -> Option<Instant> {
|
||||||
let delay = self.0.client_disconnect;
|
let delay = self.0.client_disconnect;
|
||||||
if delay != 0 {
|
if delay != 0 {
|
||||||
Some(self.0.timer.now() + Duration::from_millis(delay))
|
Some(self.now() + Duration::from_millis(delay))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Return keep-alive timer delay is configured.
|
/// Return keep-alive timer delay is configured.
|
||||||
|
#[inline]
|
||||||
pub fn keep_alive_timer(&self) -> Option<Sleep> {
|
pub fn keep_alive_timer(&self) -> Option<Sleep> {
|
||||||
if let Some(ka) = self.0.keep_alive {
|
self.keep_alive().map(|ka| sleep_until(self.now() + ka))
|
||||||
Some(sleep_until(self.0.timer.now() + ka))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keep-alive expire time
|
/// Keep-alive expire time
|
||||||
pub fn keep_alive_expire(&self) -> Option<Instant> {
|
pub fn keep_alive_expire(&self) -> Option<Instant> {
|
||||||
if let Some(ka) = self.0.keep_alive {
|
self.keep_alive().map(|ka| self.now() + ka)
|
||||||
Some(self.0.timer.now() + ka)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn now(&self) -> Instant {
|
pub(crate) fn now(&self) -> Instant {
|
||||||
self.0.timer.now()
|
self.0.date_service.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
@@ -181,7 +178,7 @@ impl ServiceConfig {
|
|||||||
let mut buf: [u8; 39] = [0; 39];
|
let mut buf: [u8; 39] = [0; 39];
|
||||||
buf[..6].copy_from_slice(b"date: ");
|
buf[..6].copy_from_slice(b"date: ");
|
||||||
self.0
|
self.0
|
||||||
.timer
|
.date_service
|
||||||
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
|
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
|
||||||
buf[35..].copy_from_slice(b"\r\n\r\n");
|
buf[35..].copy_from_slice(b"\r\n\r\n");
|
||||||
dst.extend_from_slice(&buf);
|
dst.extend_from_slice(&buf);
|
||||||
@@ -189,7 +186,7 @@ impl ServiceConfig {
|
|||||||
|
|
||||||
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
|
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
|
||||||
self.0
|
self.0
|
||||||
.timer
|
.date_service
|
||||||
.set_date(|date| dst.extend_from_slice(&date.bytes));
|
.set_date(|date| dst.extend_from_slice(&date.bytes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,12 +209,7 @@ impl Date {
|
|||||||
|
|
||||||
fn update(&mut self) {
|
fn update(&mut self) {
|
||||||
self.pos = 0;
|
self.pos = 0;
|
||||||
write!(
|
write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap();
|
||||||
self,
|
|
||||||
"{}",
|
|
||||||
OffsetDateTime::now_utc().format("%a, %d %b %Y %H:%M:%S GMT")
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,57 +222,102 @@ impl fmt::Write for Date {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
/// Service for update Date and Instant periodically at 500 millis interval.
|
||||||
struct DateService(Rc<DateServiceInner>);
|
struct DateService {
|
||||||
|
current: Rc<Cell<(Date, Instant)>>,
|
||||||
struct DateServiceInner {
|
handle: JoinHandle<()>,
|
||||||
current: Cell<Option<(Date, Instant)>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DateServiceInner {
|
impl Drop for DateService {
|
||||||
fn new() -> Self {
|
fn drop(&mut self) {
|
||||||
DateServiceInner {
|
// stop the timer update async task on drop.
|
||||||
current: Cell::new(None),
|
self.handle.abort();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(&self) {
|
|
||||||
self.current.take();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&self) {
|
|
||||||
let now = Instant::now();
|
|
||||||
let date = Date::new();
|
|
||||||
self.current.set(Some((date, now)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DateService {
|
impl DateService {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
DateService(Rc::new(DateServiceInner::new()))
|
// shared date and timer for DateService and update async task.
|
||||||
}
|
let current = Rc::new(Cell::new((Date::new(), Instant::now())));
|
||||||
|
let current_clone = Rc::clone(¤t);
|
||||||
|
// spawn an async task sleep for 500 milli and update current date/timer in a loop.
|
||||||
|
// handle is used to stop the task on DateService drop.
|
||||||
|
let handle = actix_rt::spawn(async move {
|
||||||
|
#[cfg(test)]
|
||||||
|
let _notify = notify_on_drop::NotifyOnDrop::new();
|
||||||
|
|
||||||
fn check_date(&self) {
|
let mut interval = interval(Duration::from_millis(500));
|
||||||
if self.0.current.get().is_none() {
|
loop {
|
||||||
self.0.update();
|
let now = interval.tick().await;
|
||||||
|
let date = Date::new();
|
||||||
|
current_clone.set((date, now));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// periodic date update
|
DateService { current, handle }
|
||||||
let s = self.clone();
|
|
||||||
actix_rt::spawn(sleep(Duration::from_millis(500)).then(move |_| {
|
|
||||||
s.0.reset();
|
|
||||||
future::ready(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn now(&self) -> Instant {
|
fn now(&self) -> Instant {
|
||||||
self.check_date();
|
self.current.get().1
|
||||||
self.0.current.get().unwrap().1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
|
fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
|
||||||
self.check_date();
|
f(&self.current.get().0);
|
||||||
f(&self.0.current.get().unwrap().0);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move to a util module for testing all spawn handle drop style tasks.
|
||||||
|
/// Test Module for checking the drop state of certain async tasks that are spawned
|
||||||
|
/// with `actix_rt::spawn`
|
||||||
|
///
|
||||||
|
/// The target task must explicitly generate `NotifyOnDrop` when spawn the task
|
||||||
|
#[cfg(test)]
|
||||||
|
mod notify_on_drop {
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the spawned task is dropped.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics when there was no `NotifyOnDrop` instance on current thread.
|
||||||
|
pub(crate) fn is_dropped() -> bool {
|
||||||
|
NOTIFY_DROPPED.with(|bool| {
|
||||||
|
bool.borrow()
|
||||||
|
.expect("No NotifyOnDrop existed on current thread")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct NotifyOnDrop;
|
||||||
|
|
||||||
|
impl NotifyOnDrop {
|
||||||
|
/// # Panic:
|
||||||
|
///
|
||||||
|
/// When construct multiple instances on any given thread.
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
NOTIFY_DROPPED.with(|bool| {
|
||||||
|
let mut bool = bool.borrow_mut();
|
||||||
|
if bool.is_some() {
|
||||||
|
panic!("NotifyOnDrop existed on current thread");
|
||||||
|
} else {
|
||||||
|
*bool = Some(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
NotifyOnDrop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for NotifyOnDrop {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
NOTIFY_DROPPED.with(|bool| {
|
||||||
|
if let Some(b) = bool.borrow_mut().as_mut() {
|
||||||
|
*b = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,14 +325,67 @@ impl DateService {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// Test modifying the date from within the closure
|
use actix_rt::{task::yield_now, time::sleep};
|
||||||
// passed to `set_date`
|
|
||||||
#[test]
|
#[actix_rt::test]
|
||||||
fn test_evil_date() {
|
async fn test_date_service_update() {
|
||||||
let service = DateService::new();
|
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
|
||||||
// Make sure that `check_date` doesn't try to spawn a task
|
|
||||||
service.0.update();
|
yield_now().await;
|
||||||
service.set_date(|_| service.0.reset());
|
|
||||||
|
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||||
|
settings.set_date(&mut buf1);
|
||||||
|
let now1 = settings.now();
|
||||||
|
|
||||||
|
sleep_until(Instant::now() + Duration::from_secs(2)).await;
|
||||||
|
yield_now().await;
|
||||||
|
|
||||||
|
let now2 = settings.now();
|
||||||
|
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
|
||||||
|
settings.set_date(&mut buf2);
|
||||||
|
|
||||||
|
assert_ne!(now1, now2);
|
||||||
|
|
||||||
|
assert_ne!(buf1, buf2);
|
||||||
|
|
||||||
|
drop(settings);
|
||||||
|
|
||||||
|
// Ensure the task will drop eventually
|
||||||
|
let mut times = 0;
|
||||||
|
while !notify_on_drop::is_dropped() {
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
times += 1;
|
||||||
|
assert!(times < 10, "Timeout waiting for task drop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_date_service_drop() {
|
||||||
|
let service = Rc::new(DateService::new());
|
||||||
|
|
||||||
|
// yield so date service have a chance to register the spawned timer update task.
|
||||||
|
yield_now().await;
|
||||||
|
|
||||||
|
let clone1 = service.clone();
|
||||||
|
let clone2 = service.clone();
|
||||||
|
let clone3 = service.clone();
|
||||||
|
|
||||||
|
drop(clone1);
|
||||||
|
assert!(!notify_on_drop::is_dropped());
|
||||||
|
drop(clone2);
|
||||||
|
assert!(!notify_on_drop::is_dropped());
|
||||||
|
drop(clone3);
|
||||||
|
assert!(!notify_on_drop::is_dropped());
|
||||||
|
|
||||||
|
drop(service);
|
||||||
|
|
||||||
|
// Ensure the task will drop eventually
|
||||||
|
let mut times = 0;
|
||||||
|
while !notify_on_drop::is_dropped() {
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
times += 1;
|
||||||
|
assert!(times < 10, "Timeout waiting for task drop");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,25 +1,38 @@
|
|||||||
use std::future::Future;
|
//! Stream decoders.
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
|
|
||||||
use actix_threadpool::{run, CpuFuture};
|
use std::{
|
||||||
use brotli2::write::BrotliDecoder;
|
future::Future,
|
||||||
|
io::{self, Write as _},
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_rt::task::{spawn_blocking, JoinHandle};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flate2::write::{GzDecoder, ZlibDecoder};
|
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
|
|
||||||
use super::Writer;
|
#[cfg(feature = "compress-brotli")]
|
||||||
use crate::error::PayloadError;
|
use brotli2::write::BrotliDecoder;
|
||||||
use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING};
|
|
||||||
|
|
||||||
const INPLACE: usize = 2049;
|
#[cfg(feature = "compress-gzip")]
|
||||||
|
use flate2::write::{GzDecoder, ZlibDecoder};
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
use zstd::stream::write::Decoder as ZstdDecoder;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
encoding::Writer,
|
||||||
|
error::{BlockingError, PayloadError},
|
||||||
|
header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
|
||||||
|
|
||||||
pub struct Decoder<S> {
|
pub struct Decoder<S> {
|
||||||
decoder: Option<ContentDecoder>,
|
decoder: Option<ContentDecoder>,
|
||||||
stream: S,
|
stream: S,
|
||||||
eof: bool,
|
eof: bool,
|
||||||
fut: Option<CpuFuture<(Option<Bytes>, ContentDecoder), io::Error>>,
|
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> Decoder<S>
|
impl<S> Decoder<S>
|
||||||
@@ -30,17 +43,28 @@ where
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
|
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
|
||||||
let decoder = match encoding {
|
let decoder = match encoding {
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(
|
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(
|
||||||
BrotliDecoder::new(Writer::new()),
|
BrotliDecoder::new(Writer::new()),
|
||||||
))),
|
))),
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
|
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
|
||||||
ZlibDecoder::new(Writer::new()),
|
ZlibDecoder::new(Writer::new()),
|
||||||
))),
|
))),
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(
|
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(
|
||||||
GzDecoder::new(Writer::new()),
|
GzDecoder::new(Writer::new()),
|
||||||
))),
|
))),
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
|
||||||
|
ZstdDecoder::new(Writer::new()).expect(
|
||||||
|
"Failed to create zstd decoder. This is a bug. \
|
||||||
|
Please report it to the actix-web repository.",
|
||||||
|
),
|
||||||
|
))),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Decoder {
|
Decoder {
|
||||||
decoder,
|
decoder,
|
||||||
stream,
|
stream,
|
||||||
@@ -53,15 +77,11 @@ where
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
|
pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
|
||||||
// check content-encoding
|
// check content-encoding
|
||||||
let encoding = if let Some(enc) = headers.get(&CONTENT_ENCODING) {
|
let encoding = headers
|
||||||
if let Ok(enc) = enc.to_str() {
|
.get(&CONTENT_ENCODING)
|
||||||
ContentEncoding::from(enc)
|
.and_then(|val| val.to_str().ok())
|
||||||
} else {
|
.and_then(|x| x.parse().ok())
|
||||||
ContentEncoding::Identity
|
.unwrap_or(ContentEncoding::Identity);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ContentEncoding::Identity
|
|
||||||
};
|
|
||||||
|
|
||||||
Self::new(stream, encoding)
|
Self::new(stream, encoding)
|
||||||
}
|
}
|
||||||
@@ -79,12 +99,12 @@ where
|
|||||||
) -> Poll<Option<Self::Item>> {
|
) -> Poll<Option<Self::Item>> {
|
||||||
loop {
|
loop {
|
||||||
if let Some(ref mut fut) = self.fut {
|
if let Some(ref mut fut) = self.fut {
|
||||||
let (chunk, decoder) = match ready!(Pin::new(fut).poll(cx)) {
|
let (chunk, decoder) =
|
||||||
Ok(item) => item,
|
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||||
Err(e) => return Poll::Ready(Some(Err(e.into()))),
|
|
||||||
};
|
|
||||||
self.decoder = Some(decoder);
|
self.decoder = Some(decoder);
|
||||||
self.fut.take();
|
self.fut.take();
|
||||||
|
|
||||||
if let Some(chunk) = chunk {
|
if let Some(chunk) = chunk {
|
||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
@@ -94,29 +114,34 @@ where
|
|||||||
return Poll::Ready(None);
|
return Poll::Ready(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
match Pin::new(&mut self.stream).poll_next(cx) {
|
match ready!(Pin::new(&mut self.stream).poll_next(cx)) {
|
||||||
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
|
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||||
Poll::Ready(Some(Ok(chunk))) => {
|
|
||||||
|
Some(Ok(chunk)) => {
|
||||||
if let Some(mut decoder) = self.decoder.take() {
|
if let Some(mut decoder) = self.decoder.take() {
|
||||||
if chunk.len() < INPLACE {
|
if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
|
||||||
let chunk = decoder.feed_data(chunk)?;
|
let chunk = decoder.feed_data(chunk)?;
|
||||||
self.decoder = Some(decoder);
|
self.decoder = Some(decoder);
|
||||||
|
|
||||||
if let Some(chunk) = chunk {
|
if let Some(chunk) = chunk {
|
||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.fut = Some(run(move || {
|
self.fut = Some(spawn_blocking(move || {
|
||||||
let chunk = decoder.feed_data(chunk)?;
|
let chunk = decoder.feed_data(chunk)?;
|
||||||
Ok((chunk, decoder))
|
Ok((chunk, decoder))
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Poll::Ready(None) => {
|
|
||||||
|
None => {
|
||||||
self.eof = true;
|
self.eof = true;
|
||||||
|
|
||||||
return if let Some(mut decoder) = self.decoder.take() {
|
return if let Some(mut decoder) = self.decoder.take() {
|
||||||
match decoder.feed_eof() {
|
match decoder.feed_eof() {
|
||||||
Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
|
Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
|
||||||
@@ -127,25 +152,32 @@ where
|
|||||||
Poll::Ready(None)
|
Poll::Ready(None)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Poll::Pending => break,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Poll::Pending
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ContentDecoder {
|
enum ContentDecoder {
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
Deflate(Box<ZlibDecoder<Writer>>),
|
Deflate(Box<ZlibDecoder<Writer>>),
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
Gzip(Box<GzDecoder<Writer>>),
|
Gzip(Box<GzDecoder<Writer>>),
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
Br(Box<BrotliDecoder<Writer>>),
|
Br(Box<BrotliDecoder<Writer>>),
|
||||||
|
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
|
||||||
|
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
Zstd(Box<ZstdDecoder<'static, Writer>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentDecoder {
|
impl ContentDecoder {
|
||||||
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
|
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
|
||||||
match self {
|
match self {
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
|
ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let b = decoder.get_mut().take();
|
let b = decoder.get_mut().take();
|
||||||
|
|
||||||
if !b.is_empty() {
|
if !b.is_empty() {
|
||||||
Ok(Some(b))
|
Ok(Some(b))
|
||||||
} else {
|
} else {
|
||||||
@@ -154,7 +186,23 @@ impl ContentDecoder {
|
|||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
|
ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
|
||||||
|
Ok(_) => {
|
||||||
|
let b = decoder.get_mut().take();
|
||||||
|
|
||||||
|
if !b.is_empty() {
|
||||||
|
Ok(Some(b))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
|
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let b = decoder.get_mut().take();
|
let b = decoder.get_mut().take();
|
||||||
if !b.is_empty() {
|
if !b.is_empty() {
|
||||||
@@ -165,7 +213,9 @@ impl ContentDecoder {
|
|||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
},
|
},
|
||||||
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let b = decoder.get_mut().take();
|
let b = decoder.get_mut().take();
|
||||||
if !b.is_empty() {
|
if !b.is_empty() {
|
||||||
@@ -181,10 +231,12 @@ impl ContentDecoder {
|
|||||||
|
|
||||||
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
|
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
|
||||||
match self {
|
match self {
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) {
|
ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
decoder.flush()?;
|
decoder.flush()?;
|
||||||
let b = decoder.get_mut().take();
|
let b = decoder.get_mut().take();
|
||||||
|
|
||||||
if !b.is_empty() {
|
if !b.is_empty() {
|
||||||
Ok(Some(b))
|
Ok(Some(b))
|
||||||
} else {
|
} else {
|
||||||
@@ -193,9 +245,27 @@ impl ContentDecoder {
|
|||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
|
ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
decoder.flush()?;
|
decoder.flush()?;
|
||||||
|
let b = decoder.get_mut().take();
|
||||||
|
|
||||||
|
if !b.is_empty() {
|
||||||
|
Ok(Some(b))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
|
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
|
||||||
|
Ok(_) => {
|
||||||
|
decoder.flush()?;
|
||||||
|
|
||||||
let b = decoder.get_mut().take();
|
let b = decoder.get_mut().take();
|
||||||
if !b.is_empty() {
|
if !b.is_empty() {
|
||||||
Ok(Some(b))
|
Ok(Some(b))
|
||||||
@@ -205,9 +275,12 @@ impl ContentDecoder {
|
|||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
},
|
},
|
||||||
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
decoder.flush()?;
|
decoder.flush()?;
|
||||||
|
|
||||||
let b = decoder.get_mut().take();
|
let b = decoder.get_mut().take();
|
||||||
if !b.is_empty() {
|
if !b.is_empty() {
|
||||||
Ok(Some(b))
|
Ok(Some(b))
|
||||||
|
|||||||
@@ -1,121 +1,145 @@
|
|||||||
//! Stream encoder
|
//! Stream encoders.
|
||||||
use std::future::Future;
|
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
|
|
||||||
use actix_threadpool::{run, CpuFuture};
|
use std::{
|
||||||
use brotli2::write::BrotliEncoder;
|
error::Error as StdError,
|
||||||
|
future::Future,
|
||||||
|
io::{self, Write as _},
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_rt::task::{spawn_blocking, JoinHandle};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
use derive_more::Display;
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use pin_project::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
use crate::body::{Body, BodySize, MessageBody, ResponseBody};
|
#[cfg(feature = "compress-brotli")]
|
||||||
use crate::http::header::{ContentEncoding, CONTENT_ENCODING};
|
use brotli2::write::BrotliEncoder;
|
||||||
use crate::http::{HeaderValue, StatusCode};
|
|
||||||
use crate::{Error, ResponseHead};
|
#[cfg(feature = "compress-gzip")]
|
||||||
|
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
use zstd::stream::write::Encoder as ZstdEncoder;
|
||||||
|
|
||||||
use super::Writer;
|
use super::Writer;
|
||||||
|
use crate::{
|
||||||
|
body::{BodySize, MessageBody},
|
||||||
|
error::BlockingError,
|
||||||
|
header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
|
||||||
|
ResponseHead, StatusCode,
|
||||||
|
};
|
||||||
|
|
||||||
const INPLACE: usize = 1024;
|
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
|
||||||
|
|
||||||
#[pin_project]
|
pin_project! {
|
||||||
pub struct Encoder<B> {
|
pub struct Encoder<B> {
|
||||||
eof: bool,
|
#[pin]
|
||||||
#[pin]
|
body: EncoderBody<B>,
|
||||||
body: EncoderBody<B>,
|
encoder: Option<ContentEncoder>,
|
||||||
encoder: Option<ContentEncoder>,
|
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
|
||||||
fut: Option<CpuFuture<ContentEncoder, io::Error>>,
|
eof: bool,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: MessageBody> Encoder<B> {
|
impl<B: MessageBody> Encoder<B> {
|
||||||
|
fn none() -> Self {
|
||||||
|
Encoder {
|
||||||
|
body: EncoderBody::None,
|
||||||
|
encoder: None,
|
||||||
|
fut: None,
|
||||||
|
eof: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn response(
|
pub fn response(
|
||||||
encoding: ContentEncoding,
|
encoding: ContentEncoding,
|
||||||
head: &mut ResponseHead,
|
head: &mut ResponseHead,
|
||||||
body: ResponseBody<B>,
|
body: B,
|
||||||
) -> ResponseBody<Encoder<B>> {
|
) -> Self {
|
||||||
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
||||||
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|
||||||
|| head.status == StatusCode::NO_CONTENT
|
|| head.status == StatusCode::NO_CONTENT
|
||||||
|| encoding == ContentEncoding::Identity
|
|| encoding == ContentEncoding::Identity
|
||||||
|| encoding == ContentEncoding::Auto);
|
|| encoding == ContentEncoding::Auto);
|
||||||
|
|
||||||
let body = match body {
|
match body.size() {
|
||||||
ResponseBody::Other(b) => match b {
|
// no need to compress an empty body
|
||||||
Body::None => return ResponseBody::Other(Body::None),
|
BodySize::None => return Self::none(),
|
||||||
Body::Empty => return ResponseBody::Other(Body::Empty),
|
|
||||||
Body::Bytes(buf) => {
|
// we cannot assume that Sized is not a stream
|
||||||
if can_encode {
|
BodySize::Sized(_) | BodySize::Stream => {}
|
||||||
EncoderBody::Bytes(buf)
|
}
|
||||||
} else {
|
|
||||||
return ResponseBody::Other(Body::Bytes(buf));
|
// TODO potentially some optimisation for single-chunk responses here by trying to read the
|
||||||
}
|
// payload eagerly, stopping after 2 polls if the first is a chunk and the second is None
|
||||||
}
|
|
||||||
Body::Message(stream) => EncoderBody::BoxedStream(stream),
|
|
||||||
},
|
|
||||||
ResponseBody::Body(stream) => EncoderBody::Stream(stream),
|
|
||||||
};
|
|
||||||
|
|
||||||
if can_encode {
|
if can_encode {
|
||||||
// Modify response body only if encoder is not None
|
// Modify response body only if encoder is set
|
||||||
if let Some(enc) = ContentEncoder::encoder(encoding) {
|
if let Some(enc) = ContentEncoder::encoder(encoding) {
|
||||||
update_head(encoding, head);
|
update_head(encoding, head);
|
||||||
head.no_chunking(false);
|
head.no_chunking(false);
|
||||||
return ResponseBody::Body(Encoder {
|
|
||||||
body,
|
return Encoder {
|
||||||
eof: false,
|
body: EncoderBody::Stream { body },
|
||||||
fut: None,
|
|
||||||
encoder: Some(enc),
|
encoder: Some(enc),
|
||||||
});
|
fut: None,
|
||||||
|
eof: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ResponseBody::Body(Encoder {
|
|
||||||
body,
|
Encoder {
|
||||||
eof: false,
|
body: EncoderBody::Stream { body },
|
||||||
fut: None,
|
|
||||||
encoder: None,
|
encoder: None,
|
||||||
})
|
fut: None,
|
||||||
|
eof: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pin_project(project = EncoderBodyProj)]
|
pin_project! {
|
||||||
enum EncoderBody<B> {
|
#[project = EncoderBodyProj]
|
||||||
Bytes(Bytes),
|
enum EncoderBody<B> {
|
||||||
Stream(#[pin] B),
|
None,
|
||||||
BoxedStream(Box<dyn MessageBody + Unpin>),
|
Stream { #[pin] body: B },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: MessageBody> MessageBody for EncoderBody<B> {
|
impl<B> MessageBody for EncoderBody<B>
|
||||||
|
where
|
||||||
|
B: MessageBody,
|
||||||
|
{
|
||||||
|
type Error = EncoderError;
|
||||||
|
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
match self {
|
match self {
|
||||||
EncoderBody::Bytes(ref b) => b.size(),
|
EncoderBody::None => BodySize::None,
|
||||||
EncoderBody::Stream(ref b) => b.size(),
|
EncoderBody::Stream { body } => body.size(),
|
||||||
EncoderBody::BoxedStream(ref b) => b.size(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
match self.project() {
|
match self.project() {
|
||||||
EncoderBodyProj::Bytes(b) => {
|
EncoderBodyProj::None => Poll::Ready(None),
|
||||||
if b.is_empty() {
|
|
||||||
Poll::Ready(None)
|
EncoderBodyProj::Stream { body } => body
|
||||||
} else {
|
.poll_next(cx)
|
||||||
Poll::Ready(Some(Ok(std::mem::take(b))))
|
.map_err(|err| EncoderError::Body(err.into())),
|
||||||
}
|
|
||||||
}
|
|
||||||
EncoderBodyProj::Stream(b) => b.poll_next(cx),
|
|
||||||
EncoderBodyProj::BoxedStream(ref mut b) => {
|
|
||||||
Pin::new(b.as_mut()).poll_next(cx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: MessageBody> MessageBody for Encoder<B> {
|
impl<B> MessageBody for Encoder<B>
|
||||||
|
where
|
||||||
|
B: MessageBody,
|
||||||
|
{
|
||||||
|
type Error = EncoderError;
|
||||||
|
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
if self.encoder.is_none() {
|
if self.encoder.is_none() {
|
||||||
self.body.size()
|
self.body.size()
|
||||||
@@ -127,7 +151,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
|||||||
fn poll_next(
|
fn poll_next(
|
||||||
self: Pin<&mut Self>,
|
self: Pin<&mut Self>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||||
let mut this = self.project();
|
let mut this = self.project();
|
||||||
loop {
|
loop {
|
||||||
if *this.eof {
|
if *this.eof {
|
||||||
@@ -135,32 +159,36 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mut fut) = this.fut {
|
if let Some(ref mut fut) = this.fut {
|
||||||
let mut encoder = match ready!(Pin::new(fut).poll(cx)) {
|
let mut encoder = ready!(Pin::new(fut).poll(cx))
|
||||||
Ok(item) => item,
|
.map_err(|_| EncoderError::Blocking(BlockingError))?
|
||||||
Err(e) => return Poll::Ready(Some(Err(e.into()))),
|
.map_err(EncoderError::Io)?;
|
||||||
};
|
|
||||||
let chunk = encoder.take();
|
let chunk = encoder.take();
|
||||||
*this.encoder = Some(encoder);
|
*this.encoder = Some(encoder);
|
||||||
this.fut.take();
|
this.fut.take();
|
||||||
|
|
||||||
if !chunk.is_empty() {
|
if !chunk.is_empty() {
|
||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = this.body.as_mut().poll_next(cx);
|
let result = ready!(this.body.as_mut().poll_next(cx));
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Poll::Ready(Some(Ok(chunk))) => {
|
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||||
|
|
||||||
|
Some(Ok(chunk)) => {
|
||||||
if let Some(mut encoder) = this.encoder.take() {
|
if let Some(mut encoder) = this.encoder.take() {
|
||||||
if chunk.len() < INPLACE {
|
if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE {
|
||||||
encoder.write(&chunk)?;
|
encoder.write(&chunk).map_err(EncoderError::Io)?;
|
||||||
let chunk = encoder.take();
|
let chunk = encoder.take();
|
||||||
*this.encoder = Some(encoder);
|
*this.encoder = Some(encoder);
|
||||||
|
|
||||||
if !chunk.is_empty() {
|
if !chunk.is_empty() {
|
||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
*this.fut = Some(run(move || {
|
*this.fut = Some(spawn_blocking(move || {
|
||||||
encoder.write(&chunk)?;
|
encoder.write(&chunk)?;
|
||||||
Ok(encoder)
|
Ok(encoder)
|
||||||
}));
|
}));
|
||||||
@@ -169,9 +197,11 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
|||||||
return Poll::Ready(Some(Ok(chunk)));
|
return Poll::Ready(Some(Ok(chunk)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Poll::Ready(None) => {
|
|
||||||
|
None => {
|
||||||
if let Some(encoder) = this.encoder.take() {
|
if let Some(encoder) = this.encoder.take() {
|
||||||
let chunk = encoder.finish()?;
|
let chunk = encoder.finish().map_err(EncoderError::Io)?;
|
||||||
|
|
||||||
if chunk.is_empty() {
|
if chunk.is_empty() {
|
||||||
return Poll::Ready(None);
|
return Poll::Ready(None);
|
||||||
} else {
|
} else {
|
||||||
@@ -182,7 +212,6 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
|||||||
return Poll::Ready(None);
|
return Poll::Ready(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val => return val,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,31 +219,53 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
|||||||
|
|
||||||
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
||||||
head.headers_mut().insert(
|
head.headers_mut().insert(
|
||||||
CONTENT_ENCODING,
|
header::CONTENT_ENCODING,
|
||||||
HeaderValue::from_static(encoding.as_str()),
|
HeaderValue::from_static(encoding.as_str()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ContentEncoder {
|
enum ContentEncoder {
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
Deflate(ZlibEncoder<Writer>),
|
Deflate(ZlibEncoder<Writer>),
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
Gzip(GzEncoder<Writer>),
|
Gzip(GzEncoder<Writer>),
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
Br(BrotliEncoder<Writer>),
|
Br(BrotliEncoder<Writer>),
|
||||||
|
|
||||||
|
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
|
||||||
|
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
Zstd(ZstdEncoder<'static, Writer>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentEncoder {
|
impl ContentEncoder {
|
||||||
fn encoder(encoding: ContentEncoding) -> Option<Self> {
|
fn encoder(encoding: ContentEncoding) -> Option<Self> {
|
||||||
match encoding {
|
match encoding {
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
|
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
|
||||||
Writer::new(),
|
Writer::new(),
|
||||||
flate2::Compression::fast(),
|
flate2::Compression::fast(),
|
||||||
))),
|
))),
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
|
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
|
||||||
Writer::new(),
|
Writer::new(),
|
||||||
flate2::Compression::fast(),
|
flate2::Compression::fast(),
|
||||||
))),
|
))),
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoding::Br => {
|
ContentEncoding::Br => {
|
||||||
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
|
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
ContentEncoding::Zstd => {
|
||||||
|
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
|
||||||
|
Some(ContentEncoder::Zstd(encoder))
|
||||||
|
}
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,31 +273,51 @@ impl ContentEncoder {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn take(&mut self) -> Bytes {
|
pub(crate) fn take(&mut self) -> Bytes {
|
||||||
match *self {
|
match *self {
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(self) -> Result<Bytes, io::Error> {
|
fn finish(self) -> Result<Bytes, io::Error> {
|
||||||
match self {
|
match self {
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoder::Br(encoder) => match encoder.finish() {
|
ContentEncoder::Br(encoder) => match encoder.finish() {
|
||||||
Ok(writer) => Ok(writer.buf.freeze()),
|
Ok(writer) => Ok(writer.buf.freeze()),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Gzip(encoder) => match encoder.finish() {
|
ContentEncoder::Gzip(encoder) => match encoder.finish() {
|
||||||
Ok(writer) => Ok(writer.buf.freeze()),
|
Ok(writer) => Ok(writer.buf.freeze()),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Deflate(encoder) => match encoder.finish() {
|
ContentEncoder::Deflate(encoder) => match encoder.finish() {
|
||||||
Ok(writer) => Ok(writer.buf.freeze()),
|
Ok(writer) => Ok(writer.buf.freeze()),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
ContentEncoder::Zstd(encoder) => match encoder.finish() {
|
||||||
|
Ok(writer) => Ok(writer.buf.freeze()),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
|
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
|
||||||
match *self {
|
match *self {
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -254,6 +325,8 @@ impl ContentEncoder {
|
|||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -261,6 +334,8 @@ impl ContentEncoder {
|
|||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -268,6 +343,44 @@ impl ContentEncoder {
|
|||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
trace!("Error decoding ztsd encoding: {}", err);
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Display)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum EncoderError {
|
||||||
|
#[display(fmt = "body")]
|
||||||
|
Body(Box<dyn StdError>),
|
||||||
|
|
||||||
|
#[display(fmt = "blocking")]
|
||||||
|
Blocking(BlockingError),
|
||||||
|
|
||||||
|
#[display(fmt = "io")]
|
||||||
|
Io(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StdError for EncoderError {
|
||||||
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
|
match self {
|
||||||
|
EncoderError::Body(err) => Some(&**err),
|
||||||
|
EncoderError::Blocking(err) => Some(err),
|
||||||
|
EncoderError::Io(err) => Some(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EncoderError> for crate::Error {
|
||||||
|
fn from(err: EncoderError) -> Self {
|
||||||
|
crate::Error::new_encoder().with_cause(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! Content-Encoding support
|
//! Content-Encoding support.
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
@@ -9,6 +10,9 @@ mod encoder;
|
|||||||
pub use self::decoder::Decoder;
|
pub use self::decoder::Decoder;
|
||||||
pub use self::encoder::Encoder;
|
pub use self::encoder::Encoder;
|
||||||
|
|
||||||
|
/// Special-purpose writer for streaming (de-)compression.
|
||||||
|
///
|
||||||
|
/// Pre-allocates 8KiB of capacity.
|
||||||
pub(self) struct Writer {
|
pub(self) struct Writer {
|
||||||
buf: BytesMut,
|
buf: BytesMut,
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +1,118 @@
|
|||||||
use std::any::{Any, TypeId};
|
use std::{
|
||||||
use std::{fmt, mem};
|
any::{Any, TypeId},
|
||||||
|
fmt,
|
||||||
|
};
|
||||||
|
|
||||||
use fxhash::FxHashMap;
|
use ahash::AHashMap;
|
||||||
|
|
||||||
/// A type map of request extensions.
|
/// A type map for request extensions.
|
||||||
|
///
|
||||||
|
/// All entries into this map must be owned types (or static references).
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Extensions {
|
pub struct Extensions {
|
||||||
/// Use FxHasher with a std HashMap with for faster
|
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
|
||||||
/// lookups on the small `TypeId` (u64 equivalent) keys.
|
map: AHashMap<TypeId, Box<dyn Any>>,
|
||||||
map: FxHashMap<TypeId, Box<dyn Any>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Extensions {
|
impl Extensions {
|
||||||
/// Create an empty `Extensions`.
|
/// Creates an empty `Extensions`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new() -> Extensions {
|
pub fn new() -> Extensions {
|
||||||
Extensions {
|
Extensions {
|
||||||
map: FxHashMap::default(),
|
map: AHashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a type into this `Extensions`.
|
/// Insert an item into the map.
|
||||||
///
|
///
|
||||||
/// If a extension of this type already existed, it will
|
/// If an item of this type was already stored, it will be replaced and returned.
|
||||||
/// be returned.
|
///
|
||||||
pub fn insert<T: 'static>(&mut self, val: T) {
|
/// ```
|
||||||
self.map.insert(TypeId::of::<T>(), Box::new(val));
|
/// # use actix_http::Extensions;
|
||||||
|
/// let mut map = Extensions::new();
|
||||||
|
/// assert_eq!(map.insert(""), None);
|
||||||
|
/// assert_eq!(map.insert(1u32), None);
|
||||||
|
/// assert_eq!(map.insert(2u32), Some(1u32));
|
||||||
|
/// assert_eq!(*map.get::<u32>().unwrap(), 2u32);
|
||||||
|
/// ```
|
||||||
|
pub fn insert<T: 'static>(&mut self, val: T) -> Option<T> {
|
||||||
|
self.map
|
||||||
|
.insert(TypeId::of::<T>(), Box::new(val))
|
||||||
|
.and_then(downcast_owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if container contains entry
|
/// Check if map contains an item of a given type.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::Extensions;
|
||||||
|
/// let mut map = Extensions::new();
|
||||||
|
/// assert!(!map.contains::<u32>());
|
||||||
|
///
|
||||||
|
/// assert_eq!(map.insert(1u32), None);
|
||||||
|
/// assert!(map.contains::<u32>());
|
||||||
|
/// ```
|
||||||
pub fn contains<T: 'static>(&self) -> bool {
|
pub fn contains<T: 'static>(&self) -> bool {
|
||||||
self.map.contains_key(&TypeId::of::<T>())
|
self.map.contains_key(&TypeId::of::<T>())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to a type previously inserted on this `Extensions`.
|
/// Get a reference to an item of a given type.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::Extensions;
|
||||||
|
/// let mut map = Extensions::new();
|
||||||
|
/// map.insert(1u32);
|
||||||
|
/// assert_eq!(map.get::<u32>(), Some(&1u32));
|
||||||
|
/// ```
|
||||||
pub fn get<T: 'static>(&self) -> Option<&T> {
|
pub fn get<T: 'static>(&self) -> Option<&T> {
|
||||||
self.map
|
self.map
|
||||||
.get(&TypeId::of::<T>())
|
.get(&TypeId::of::<T>())
|
||||||
.and_then(|boxed| boxed.downcast_ref())
|
.and_then(|boxed| boxed.downcast_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a mutable reference to a type previously inserted on this `Extensions`.
|
/// Get a mutable reference to an item of a given type.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::Extensions;
|
||||||
|
/// let mut map = Extensions::new();
|
||||||
|
/// map.insert(1u32);
|
||||||
|
/// assert_eq!(map.get_mut::<u32>(), Some(&mut 1u32));
|
||||||
|
/// ```
|
||||||
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
|
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
|
||||||
self.map
|
self.map
|
||||||
.get_mut(&TypeId::of::<T>())
|
.get_mut(&TypeId::of::<T>())
|
||||||
.and_then(|boxed| boxed.downcast_mut())
|
.and_then(|boxed| boxed.downcast_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a type from this `Extensions`.
|
/// Remove an item from the map of a given type.
|
||||||
///
|
///
|
||||||
/// If a extension of this type existed, it will be returned.
|
/// If an item of this type was already stored, it will be returned.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::Extensions;
|
||||||
|
/// let mut map = Extensions::new();
|
||||||
|
///
|
||||||
|
/// map.insert(1u32);
|
||||||
|
/// assert_eq!(map.get::<u32>(), Some(&1u32));
|
||||||
|
///
|
||||||
|
/// assert_eq!(map.remove::<u32>(), Some(1u32));
|
||||||
|
/// assert!(!map.contains::<u32>());
|
||||||
|
/// ```
|
||||||
pub fn remove<T: 'static>(&mut self) -> Option<T> {
|
pub fn remove<T: 'static>(&mut self) -> Option<T> {
|
||||||
self.map
|
self.map.remove(&TypeId::of::<T>()).and_then(downcast_owned)
|
||||||
.remove(&TypeId::of::<T>())
|
|
||||||
.and_then(|boxed| boxed.downcast().ok().map(|boxed| *boxed))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the `Extensions` of all inserted extensions.
|
/// Clear the `Extensions` of all inserted extensions.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::Extensions;
|
||||||
|
/// let mut map = Extensions::new();
|
||||||
|
///
|
||||||
|
/// map.insert(1u32);
|
||||||
|
/// assert!(map.contains::<u32>());
|
||||||
|
///
|
||||||
|
/// map.clear();
|
||||||
|
/// assert!(!map.contains::<u32>());
|
||||||
|
/// ```
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.map.clear();
|
self.map.clear();
|
||||||
@@ -67,9 +123,11 @@ impl Extensions {
|
|||||||
self.map.extend(other.map);
|
self.map.extend(other.map);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets (or overrides) items from `other` into this map.
|
/// Sets (or overrides) items from cloneable extensions map into this map.
|
||||||
pub(crate) fn drain_from(&mut self, other: &mut Self) {
|
pub(crate) fn clone_from(&mut self, other: &CloneableExtensions) {
|
||||||
self.map.extend(mem::take(&mut other.map));
|
for (k, val) in &other.map {
|
||||||
|
self.map.insert(*k, (**val).clone_to_any());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +137,108 @@ impl fmt::Debug for Extensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn downcast_owned<T: 'static>(boxed: Box<dyn Any>) -> Option<T> {
|
||||||
|
boxed.downcast().ok().map(|boxed| *boxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub trait CloneToAny {
|
||||||
|
/// Cast `self` into an `Any` reference.
|
||||||
|
#[cfg(test)]
|
||||||
|
fn any_ref(&self) -> &dyn Any;
|
||||||
|
|
||||||
|
/// Clone `self` to a new `Box<Any>` object.
|
||||||
|
fn clone_to_any(&self) -> Box<dyn Any>;
|
||||||
|
|
||||||
|
/// Clone `self` to a new `Box<CloneAny>` object.
|
||||||
|
fn clone_to_clone_any(&self) -> Box<dyn CloneAny>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Any> CloneToAny for T {
|
||||||
|
#[cfg(test)]
|
||||||
|
fn any_ref(&self) -> &dyn Any {
|
||||||
|
&*self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn clone_to_any(&self) -> Box<dyn Any> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn clone_to_clone_any(&self) -> Box<dyn CloneAny> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An [`Any`] trait with an additional [`Clone`] requirement.
|
||||||
|
pub trait CloneAny: CloneToAny + Any {}
|
||||||
|
impl<T: Any + Clone> CloneAny for T {}
|
||||||
|
|
||||||
|
impl Clone for Box<dyn CloneAny> {
|
||||||
|
#[inline]
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
(**self).clone_to_clone_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait UncheckedAnyExt {
|
||||||
|
/// # Safety
|
||||||
|
/// Caller must ensure type `T` is true type.
|
||||||
|
#[inline]
|
||||||
|
unsafe fn downcast_unchecked<T: 'static>(self: Box<Self>) -> Box<T> {
|
||||||
|
Box::from_raw(Box::into_raw(self) as *mut T)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UncheckedAnyExt for dyn CloneAny {}
|
||||||
|
|
||||||
|
/// A type map for `on_connect` extensions.
|
||||||
|
///
|
||||||
|
/// All entries into this map must be owned types and implement `Clone` trait.
|
||||||
|
///
|
||||||
|
/// Many requests can be processed for each connection but the `on_connect` will only be run once
|
||||||
|
/// when the connection is opened. Therefore, items added to this special map type need to be cloned
|
||||||
|
/// into the regular extensions map for each request. Most useful connection information types are
|
||||||
|
/// cloneable already but you can use reference counted wrappers if not.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CloneableExtensions {
|
||||||
|
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
|
||||||
|
map: AHashMap<TypeId, Box<dyn CloneAny>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloneableExtensions {
|
||||||
|
/// Insert an item into the map.
|
||||||
|
///
|
||||||
|
/// If an item of this type was already stored, it will be replaced and returned.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use actix_http::Extensions;
|
||||||
|
/// let mut map = Extensions::new();
|
||||||
|
/// assert_eq!(map.insert(""), None);
|
||||||
|
/// assert_eq!(map.insert(1u32), None);
|
||||||
|
/// assert_eq!(map.insert(2u32), Some(1u32));
|
||||||
|
/// assert_eq!(*map.get::<u32>().unwrap(), 2u32);
|
||||||
|
/// ```
|
||||||
|
pub fn insert<T: CloneAny>(&mut self, val: T) -> Option<T> {
|
||||||
|
self.map
|
||||||
|
.insert(TypeId::of::<T>(), Box::new(val))
|
||||||
|
.map(|boxed| {
|
||||||
|
// Safety:
|
||||||
|
// Box is owned and `T` is known to be true type from map.
|
||||||
|
*unsafe { UncheckedAnyExt::downcast_unchecked::<T>(boxed) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn get<T: CloneAny>(&self) -> Option<&T> {
|
||||||
|
self.map
|
||||||
|
.get(&TypeId::of::<T>())
|
||||||
|
.and_then(|boxed| boxed.as_ref().any_ref().downcast_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -118,6 +278,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_integers() {
|
fn test_integers() {
|
||||||
|
static A: u32 = 8;
|
||||||
|
|
||||||
let mut map = Extensions::new();
|
let mut map = Extensions::new();
|
||||||
|
|
||||||
map.insert::<i8>(8);
|
map.insert::<i8>(8);
|
||||||
@@ -130,6 +292,7 @@ mod tests {
|
|||||||
map.insert::<u32>(32);
|
map.insert::<u32>(32);
|
||||||
map.insert::<u64>(64);
|
map.insert::<u64>(64);
|
||||||
map.insert::<u128>(128);
|
map.insert::<u128>(128);
|
||||||
|
map.insert::<&'static u32>(&A);
|
||||||
assert!(map.get::<i8>().is_some());
|
assert!(map.get::<i8>().is_some());
|
||||||
assert!(map.get::<i16>().is_some());
|
assert!(map.get::<i16>().is_some());
|
||||||
assert!(map.get::<i32>().is_some());
|
assert!(map.get::<i32>().is_some());
|
||||||
@@ -140,6 +303,7 @@ mod tests {
|
|||||||
assert!(map.get::<u32>().is_some());
|
assert!(map.get::<u32>().is_some());
|
||||||
assert!(map.get::<u64>().is_some());
|
assert!(map.get::<u64>().is_some());
|
||||||
assert!(map.get::<u128>().is_some());
|
assert!(map.get::<u128>().is_some());
|
||||||
|
assert!(map.get::<&'static u32>().is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -220,25 +384,41 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_drain_from() {
|
fn test_clone_from() {
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct NonCopy {
|
||||||
|
num: u8,
|
||||||
|
}
|
||||||
|
|
||||||
let mut ext = Extensions::new();
|
let mut ext = Extensions::new();
|
||||||
ext.insert(2isize);
|
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::<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);
|
let mut more_ext = CloneableExtensions::default();
|
||||||
|
more_ext.insert(3isize);
|
||||||
|
more_ext.insert(3usize);
|
||||||
|
more_ext.insert(NonCopy { num: 8 });
|
||||||
|
|
||||||
assert_eq!(ext.get::<isize>(), Some(&5isize));
|
ext.clone_from(&more_ext);
|
||||||
assert_eq!(ext.get::<usize>(), Some(&5usize));
|
|
||||||
assert_eq!(more_ext.get::<isize>(), None);
|
assert_eq!(ext.get::<isize>(), Some(&3isize));
|
||||||
assert_eq!(more_ext.get::<usize>(), None);
|
assert_eq!(ext.get::<usize>(), Some(&3usize));
|
||||||
|
assert_eq!(more_ext.get::<isize>(), Some(&3isize));
|
||||||
|
assert_eq!(more_ext.get::<usize>(), Some(&3usize));
|
||||||
|
|
||||||
|
assert!(ext.get::<NonCopy>().is_some());
|
||||||
|
assert!(more_ext.get::<NonCopy>().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boxes_not_aliased() {
|
||||||
|
let a: Box<dyn CloneAny> = Box::new(42);
|
||||||
|
let b = a.clone_to_clone_any();
|
||||||
|
assert_ne!(Box::into_raw(a) as *const (), Box::into_raw(b) as *const ());
|
||||||
|
|
||||||
|
let a: Box<dyn CloneAny> = Box::new(42);
|
||||||
|
let b = a.clone_to_any();
|
||||||
|
assert_ne!(Box::into_raw(a) as *const (), Box::into_raw(b) as *const ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
432
actix-http/src/h1/chunked.rs
Normal file
432
actix-http/src/h1/chunked.rs
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
use std::{io, task::Poll};
|
||||||
|
|
||||||
|
use bytes::{Buf as _, Bytes, BytesMut};
|
||||||
|
|
||||||
|
macro_rules! byte (
|
||||||
|
($rdr:ident) => ({
|
||||||
|
if $rdr.len() > 0 {
|
||||||
|
let b = $rdr[0];
|
||||||
|
$rdr.advance(1);
|
||||||
|
b
|
||||||
|
} else {
|
||||||
|
return Poll::Pending
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub(super) enum ChunkedState {
|
||||||
|
Size,
|
||||||
|
SizeLws,
|
||||||
|
Extension,
|
||||||
|
SizeLf,
|
||||||
|
Body,
|
||||||
|
BodyCr,
|
||||||
|
BodyLf,
|
||||||
|
EndCr,
|
||||||
|
EndLf,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkedState {
|
||||||
|
pub(super) fn step(
|
||||||
|
&self,
|
||||||
|
body: &mut BytesMut,
|
||||||
|
size: &mut u64,
|
||||||
|
buf: &mut Option<Bytes>,
|
||||||
|
) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
use self::ChunkedState::*;
|
||||||
|
match *self {
|
||||||
|
Size => ChunkedState::read_size(body, size),
|
||||||
|
SizeLws => ChunkedState::read_size_lws(body),
|
||||||
|
Extension => ChunkedState::read_extension(body),
|
||||||
|
SizeLf => ChunkedState::read_size_lf(body, *size),
|
||||||
|
Body => ChunkedState::read_body(body, size, buf),
|
||||||
|
BodyCr => ChunkedState::read_body_cr(body),
|
||||||
|
BodyLf => ChunkedState::read_body_lf(body),
|
||||||
|
EndCr => ChunkedState::read_end_cr(body),
|
||||||
|
EndLf => ChunkedState::read_end_lf(body),
|
||||||
|
End => Poll::Ready(Ok(ChunkedState::End)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_size(
|
||||||
|
rdr: &mut BytesMut,
|
||||||
|
size: &mut u64,
|
||||||
|
) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
let radix = 16;
|
||||||
|
|
||||||
|
let rem = match byte!(rdr) {
|
||||||
|
b @ b'0'..=b'9' => b - b'0',
|
||||||
|
b @ b'a'..=b'f' => b + 10 - b'a',
|
||||||
|
b @ b'A'..=b'F' => b + 10 - b'A',
|
||||||
|
b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)),
|
||||||
|
b';' => return Poll::Ready(Ok(ChunkedState::Extension)),
|
||||||
|
b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)),
|
||||||
|
_ => {
|
||||||
|
return Poll::Ready(Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Invalid chunk size line: Invalid Size",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match size.checked_mul(radix) {
|
||||||
|
Some(n) => {
|
||||||
|
*size = n as u64;
|
||||||
|
*size += rem as u64;
|
||||||
|
|
||||||
|
Poll::Ready(Ok(ChunkedState::Size))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
log::debug!("chunk size would overflow u64");
|
||||||
|
Poll::Ready(Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Invalid chunk size line: Size is too big",
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_size_lws(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
match byte!(rdr) {
|
||||||
|
// LWS can follow the chunk size, but no more digits can come
|
||||||
|
b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)),
|
||||||
|
b';' => Poll::Ready(Ok(ChunkedState::Extension)),
|
||||||
|
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
|
||||||
|
_ => Poll::Ready(Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Invalid chunk size linear white space",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn read_extension(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
match byte!(rdr) {
|
||||||
|
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
|
||||||
|
// strictly 0x20 (space) should be disallowed but we don't parse quoted strings here
|
||||||
|
0x00..=0x08 | 0x0a..=0x1f | 0x7f => Poll::Ready(Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Invalid character in chunk extension",
|
||||||
|
))),
|
||||||
|
_ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn read_size_lf(
|
||||||
|
rdr: &mut BytesMut,
|
||||||
|
size: u64,
|
||||||
|
) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
match byte!(rdr) {
|
||||||
|
b'\n' if size > 0 => Poll::Ready(Ok(ChunkedState::Body)),
|
||||||
|
b'\n' if size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)),
|
||||||
|
_ => Poll::Ready(Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Invalid chunk size LF",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_body(
|
||||||
|
rdr: &mut BytesMut,
|
||||||
|
rem: &mut u64,
|
||||||
|
buf: &mut Option<Bytes>,
|
||||||
|
) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
log::trace!("Chunked read, remaining={:?}", rem);
|
||||||
|
|
||||||
|
let len = rdr.len() as u64;
|
||||||
|
if len == 0 {
|
||||||
|
Poll::Ready(Ok(ChunkedState::Body))
|
||||||
|
} else {
|
||||||
|
let slice;
|
||||||
|
if *rem > len {
|
||||||
|
slice = rdr.split().freeze();
|
||||||
|
*rem -= len;
|
||||||
|
} else {
|
||||||
|
slice = rdr.split_to(*rem as usize).freeze();
|
||||||
|
*rem = 0;
|
||||||
|
}
|
||||||
|
*buf = Some(slice);
|
||||||
|
if *rem > 0 {
|
||||||
|
Poll::Ready(Ok(ChunkedState::Body))
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Ok(ChunkedState::BodyCr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_body_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
match byte!(rdr) {
|
||||||
|
b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)),
|
||||||
|
_ => Poll::Ready(Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Invalid chunk body CR",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn read_body_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
match byte!(rdr) {
|
||||||
|
b'\n' => Poll::Ready(Ok(ChunkedState::Size)),
|
||||||
|
_ => Poll::Ready(Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Invalid chunk body LF",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn read_end_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
match byte!(rdr) {
|
||||||
|
b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)),
|
||||||
|
_ => Poll::Ready(Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Invalid chunk end CR",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn read_end_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
|
match byte!(rdr) {
|
||||||
|
b'\n' => Poll::Ready(Ok(ChunkedState::End)),
|
||||||
|
_ => Poll::Ready(Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Invalid chunk end LF",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use actix_codec::Decoder as _;
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use http::Method;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::ParseError,
|
||||||
|
h1::decoder::{MessageDecoder, PayloadItem},
|
||||||
|
HttpMessage as _, Request,
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! parse_ready {
|
||||||
|
($e:expr) => {{
|
||||||
|
match MessageDecoder::<Request>::default().decode($e) {
|
||||||
|
Ok(Some((msg, _))) => msg,
|
||||||
|
Ok(_) => unreachable!("Eof during parsing http request"),
|
||||||
|
Err(err) => unreachable!("Error during parsing http request: {:?}", err),
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! expect_parse_err {
|
||||||
|
($e:expr) => {{
|
||||||
|
match MessageDecoder::<Request>::default().decode($e) {
|
||||||
|
Err(err) => match err {
|
||||||
|
ParseError::Io(_) => unreachable!("Parse error expected"),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => unreachable!("Error expected"),
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_chunked_payload_chunk_extension() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test HTTP/1.1\r\n\
|
||||||
|
transfer-encoding: chunked\r\n\
|
||||||
|
\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
let mut pl = pl.unwrap();
|
||||||
|
assert!(msg.chunked().unwrap());
|
||||||
|
|
||||||
|
buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n")
|
||||||
|
let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk();
|
||||||
|
assert_eq!(chunk, Bytes::from_static(b"data"));
|
||||||
|
let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk();
|
||||||
|
assert_eq!(chunk, Bytes::from_static(b"line"));
|
||||||
|
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert!(msg.eof());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_request_chunked() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test HTTP/1.1\r\n\
|
||||||
|
transfer-encoding: chunked\r\n\r\n",
|
||||||
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
|
if let Ok(val) = req.chunked() {
|
||||||
|
assert!(val);
|
||||||
|
} else {
|
||||||
|
unreachable!("Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// intentional typo in "chunked"
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test HTTP/1.1\r\n\
|
||||||
|
transfer-encoding: chnked\r\n\r\n",
|
||||||
|
);
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_http_request_chunked_payload() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test HTTP/1.1\r\n\
|
||||||
|
transfer-encoding: chunked\r\n\r\n",
|
||||||
|
);
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
let mut pl = pl.unwrap();
|
||||||
|
assert!(req.chunked().unwrap());
|
||||||
|
|
||||||
|
buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n");
|
||||||
|
assert_eq!(
|
||||||
|
pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(),
|
||||||
|
b"data"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(),
|
||||||
|
b"line"
|
||||||
|
);
|
||||||
|
assert!(pl.decode(&mut buf).unwrap().unwrap().eof());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_http_request_chunked_payload_and_next_message() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test HTTP/1.1\r\n\
|
||||||
|
transfer-encoding: chunked\r\n\r\n",
|
||||||
|
);
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
let mut pl = pl.unwrap();
|
||||||
|
assert!(req.chunked().unwrap());
|
||||||
|
|
||||||
|
buf.extend(
|
||||||
|
b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\
|
||||||
|
POST /test2 HTTP/1.1\r\n\
|
||||||
|
transfer-encoding: chunked\r\n\r\n"
|
||||||
|
.iter(),
|
||||||
|
);
|
||||||
|
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(msg.chunk().as_ref(), b"data");
|
||||||
|
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(msg.chunk().as_ref(), b"line");
|
||||||
|
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert!(msg.eof());
|
||||||
|
|
||||||
|
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert!(req.chunked().unwrap());
|
||||||
|
assert_eq!(*req.method(), Method::POST);
|
||||||
|
assert!(req.chunked().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_http_request_chunked_payload_chunks() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test HTTP/1.1\r\n\
|
||||||
|
transfer-encoding: chunked\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
let mut pl = pl.unwrap();
|
||||||
|
assert!(req.chunked().unwrap());
|
||||||
|
|
||||||
|
buf.extend(b"4\r\n1111\r\n");
|
||||||
|
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(msg.chunk().as_ref(), b"1111");
|
||||||
|
|
||||||
|
buf.extend(b"4\r\ndata\r");
|
||||||
|
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(msg.chunk().as_ref(), b"data");
|
||||||
|
|
||||||
|
buf.extend(b"\n4");
|
||||||
|
assert!(pl.decode(&mut buf).unwrap().is_none());
|
||||||
|
|
||||||
|
buf.extend(b"\r");
|
||||||
|
assert!(pl.decode(&mut buf).unwrap().is_none());
|
||||||
|
buf.extend(b"\n");
|
||||||
|
assert!(pl.decode(&mut buf).unwrap().is_none());
|
||||||
|
|
||||||
|
buf.extend(b"li");
|
||||||
|
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(msg.chunk().as_ref(), b"li");
|
||||||
|
|
||||||
|
//trailers
|
||||||
|
//buf.feed_data("test: test\r\n");
|
||||||
|
//not_ready!(reader.parse(&mut buf, &mut readbuf));
|
||||||
|
|
||||||
|
buf.extend(b"ne\r\n0\r\n");
|
||||||
|
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(msg.chunk().as_ref(), b"ne");
|
||||||
|
assert!(pl.decode(&mut buf).unwrap().is_none());
|
||||||
|
|
||||||
|
buf.extend(b"\r\n");
|
||||||
|
assert!(pl.decode(&mut buf).unwrap().unwrap().eof());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chunk_extension_quoted() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test HTTP/1.1\r\n\
|
||||||
|
Host: localhost:8080\r\n\
|
||||||
|
Transfer-Encoding: chunked\r\n\
|
||||||
|
\r\n\
|
||||||
|
2;hello=b;one=\"1 2 3\"\r\n\
|
||||||
|
xx",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
let mut pl = pl.unwrap();
|
||||||
|
|
||||||
|
let chunk = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"xx")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hrs_chunk_extension_invalid() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET / HTTP/1.1\r\n\
|
||||||
|
Host: localhost:8080\r\n\
|
||||||
|
Transfer-Encoding: chunked\r\n\
|
||||||
|
\r\n\
|
||||||
|
2;x\nx\r\n\
|
||||||
|
4c\r\n\
|
||||||
|
0\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
let mut pl = pl.unwrap();
|
||||||
|
|
||||||
|
let err = pl.decode(&mut buf).unwrap_err();
|
||||||
|
assert!(err
|
||||||
|
.to_string()
|
||||||
|
.contains("Invalid character in chunk extension"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hrs_chunk_size_overflow() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET / HTTP/1.1\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Transfer-Encoding: chunked\r\n\
|
||||||
|
\r\n\
|
||||||
|
f0000000000000003\r\n\
|
||||||
|
abc\r\n\
|
||||||
|
0\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
let mut pl = pl.unwrap();
|
||||||
|
|
||||||
|
let err = pl.decode(&mut buf).unwrap_err();
|
||||||
|
assert!(err
|
||||||
|
.to_string()
|
||||||
|
.contains("Invalid chunk size line: Size is too big"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,7 +120,7 @@ impl Decoder for ClientCodec {
|
|||||||
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
|
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
|
||||||
|
|
||||||
if let Some((req, payload)) = self.inner.decoder.decode(src)? {
|
if let Some((req, payload)) = self.inner.decoder.decode(src)? {
|
||||||
if let Some(ctype) = req.ctype() {
|
if let Some(ctype) = req.conn_type() {
|
||||||
// do not use peer's keep-alive
|
// do not use peer's keep-alive
|
||||||
self.inner.ctype = if ctype == ConnectionType::KeepAlive {
|
self.inner.ctype = if ctype == ConnectionType::KeepAlive {
|
||||||
self.inner.ctype
|
self.inner.ctype
|
||||||
@@ -223,15 +223,3 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Writer<'a>(pub &'a mut BytesMut);
|
|
||||||
|
|
||||||
impl<'a> io::Write for Writer<'a> {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
self.0.extend_from_slice(buf);
|
|
||||||
Ok(buf.len())
|
|
||||||
}
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ pub struct Codec {
|
|||||||
decoder: decoder::MessageDecoder<Request>,
|
decoder: decoder::MessageDecoder<Request>,
|
||||||
payload: Option<PayloadDecoder>,
|
payload: Option<PayloadDecoder>,
|
||||||
version: Version,
|
version: Version,
|
||||||
ctype: ConnectionType,
|
conn_type: ConnectionType,
|
||||||
|
|
||||||
// encoder part
|
// encoder part
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
@@ -65,7 +65,7 @@ impl Codec {
|
|||||||
decoder: decoder::MessageDecoder::default(),
|
decoder: decoder::MessageDecoder::default(),
|
||||||
payload: None,
|
payload: None,
|
||||||
version: Version::HTTP_11,
|
version: Version::HTTP_11,
|
||||||
ctype: ConnectionType::Close,
|
conn_type: ConnectionType::Close,
|
||||||
encoder: encoder::MessageEncoder::default(),
|
encoder: encoder::MessageEncoder::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,13 +73,13 @@ impl Codec {
|
|||||||
/// Check if request is upgrade.
|
/// Check if request is upgrade.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn upgrade(&self) -> bool {
|
pub fn upgrade(&self) -> bool {
|
||||||
self.ctype == ConnectionType::Upgrade
|
self.conn_type == ConnectionType::Upgrade
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if last response is keep-alive.
|
/// Check if last response is keep-alive.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn keepalive(&self) -> bool {
|
pub fn keepalive(&self) -> bool {
|
||||||
self.ctype == ConnectionType::KeepAlive
|
self.conn_type == ConnectionType::KeepAlive
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if keep-alive enabled on server level.
|
/// Check if keep-alive enabled on server level.
|
||||||
@@ -124,11 +124,11 @@ impl Decoder for Codec {
|
|||||||
let head = req.head();
|
let head = req.head();
|
||||||
self.flags.set(Flags::HEAD, head.method == Method::HEAD);
|
self.flags.set(Flags::HEAD, head.method == Method::HEAD);
|
||||||
self.version = head.version;
|
self.version = head.version;
|
||||||
self.ctype = head.connection_type();
|
self.conn_type = head.connection_type();
|
||||||
if self.ctype == ConnectionType::KeepAlive
|
if self.conn_type == ConnectionType::KeepAlive
|
||||||
&& !self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
&& !self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
||||||
{
|
{
|
||||||
self.ctype = ConnectionType::Close
|
self.conn_type = ConnectionType::Close
|
||||||
}
|
}
|
||||||
match payload {
|
match payload {
|
||||||
PayloadType::None => self.payload = None,
|
PayloadType::None => self.payload = None,
|
||||||
@@ -159,14 +159,14 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
|||||||
res.head_mut().version = self.version;
|
res.head_mut().version = self.version;
|
||||||
|
|
||||||
// connection status
|
// connection status
|
||||||
self.ctype = if let Some(ct) = res.head().ctype() {
|
self.conn_type = if let Some(ct) = res.head().conn_type() {
|
||||||
if ct == ConnectionType::KeepAlive {
|
if ct == ConnectionType::KeepAlive {
|
||||||
self.ctype
|
self.conn_type
|
||||||
} else {
|
} else {
|
||||||
ct
|
ct
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.ctype
|
self.conn_type
|
||||||
};
|
};
|
||||||
|
|
||||||
// encode message
|
// encode message
|
||||||
@@ -177,10 +177,9 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
|||||||
self.flags.contains(Flags::STREAM),
|
self.flags.contains(Flags::STREAM),
|
||||||
self.version,
|
self.version,
|
||||||
length,
|
length,
|
||||||
self.ctype,
|
self.conn_type,
|
||||||
&self.config,
|
&self.config,
|
||||||
)?;
|
)?;
|
||||||
// self.headers_size = (dst.len() - len) as u32;
|
|
||||||
}
|
}
|
||||||
Message::Chunk(Some(bytes)) => {
|
Message::Chunk(Some(bytes)) => {
|
||||||
self.encoder.encode_chunk(bytes.as_ref(), dst)?;
|
self.encoder.encode_chunk(bytes.as_ref(), dst)?;
|
||||||
@@ -189,6 +188,7 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
|||||||
self.encoder.encode_eof(dst)?;
|
self.encoder.encode_eof(dst)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,10 +199,10 @@ mod tests {
|
|||||||
use http::Method;
|
use http::Method;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::httpmessage::HttpMessage;
|
use crate::HttpMessage;
|
||||||
|
|
||||||
#[test]
|
#[actix_rt::test]
|
||||||
fn test_http_request_chunked_payload_and_next_message() {
|
async fn test_http_request_chunked_payload_and_next_message() {
|
||||||
let mut codec = Codec::default();
|
let mut codec = Codec::default();
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
use std::convert::TryFrom;
|
use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Poll};
|
||||||
use std::io;
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::task::Poll;
|
|
||||||
|
|
||||||
use actix_codec::Decoder;
|
use actix_codec::Decoder;
|
||||||
use bytes::{Buf, Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use http::header::{HeaderName, HeaderValue};
|
use http::header::{HeaderName, HeaderValue};
|
||||||
use http::{header, Method, StatusCode, Uri, Version};
|
use http::{header, Method, StatusCode, Uri, Version};
|
||||||
use log::{debug, error, trace};
|
use log::{debug, error, trace};
|
||||||
|
|
||||||
use crate::error::ParseError;
|
use super::chunked::ChunkedState;
|
||||||
use crate::header::HeaderMap;
|
use crate::{
|
||||||
use crate::message::{ConnectionType, ResponseHead};
|
error::ParseError,
|
||||||
use crate::request::Request;
|
header::HeaderMap,
|
||||||
|
message::{ConnectionType, ResponseHead},
|
||||||
|
request::Request,
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_BUFFER_SIZE: usize = 131_072;
|
pub(crate) const MAX_BUFFER_SIZE: usize = 131_072;
|
||||||
const MAX_HEADERS: usize = 96;
|
const MAX_HEADERS: usize = 96;
|
||||||
|
|
||||||
/// Incoming message decoder
|
/// Incoming message decoder
|
||||||
@@ -67,6 +67,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
let mut has_upgrade_websocket = false;
|
let mut has_upgrade_websocket = false;
|
||||||
let mut expect = false;
|
let mut expect = false;
|
||||||
let mut chunked = false;
|
let mut chunked = false;
|
||||||
|
let mut seen_te = false;
|
||||||
let mut content_length = None;
|
let mut content_length = None;
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -85,8 +86,17 @@ pub(crate) trait MessageType: Sized {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match name {
|
match name {
|
||||||
header::CONTENT_LENGTH => {
|
header::CONTENT_LENGTH if content_length.is_some() => {
|
||||||
if let Ok(s) = value.to_str() {
|
debug!("multiple Content-Length");
|
||||||
|
return Err(ParseError::Header);
|
||||||
|
}
|
||||||
|
|
||||||
|
header::CONTENT_LENGTH => match value.to_str() {
|
||||||
|
Ok(s) if s.trim().starts_with('+') => {
|
||||||
|
debug!("illegal Content-Length: {:?}", s);
|
||||||
|
return Err(ParseError::Header);
|
||||||
|
}
|
||||||
|
Ok(s) => {
|
||||||
if let Ok(len) = s.parse::<u64>() {
|
if let Ok(len) = s.parse::<u64>() {
|
||||||
if len != 0 {
|
if len != 0 {
|
||||||
content_length = Some(len);
|
content_length = Some(len);
|
||||||
@@ -95,22 +105,38 @@ pub(crate) trait MessageType: Sized {
|
|||||||
debug!("illegal Content-Length: {:?}", s);
|
debug!("illegal Content-Length: {:?}", s);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
Err(_) => {
|
||||||
debug!("illegal Content-Length: {:?}", value);
|
debug!("illegal Content-Length: {:?}", value);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
// transfer-encoding
|
// transfer-encoding
|
||||||
|
header::TRANSFER_ENCODING if seen_te => {
|
||||||
|
debug!("multiple Transfer-Encoding not allowed");
|
||||||
|
return Err(ParseError::Header);
|
||||||
|
}
|
||||||
|
|
||||||
header::TRANSFER_ENCODING => {
|
header::TRANSFER_ENCODING => {
|
||||||
if let Ok(s) = value.to_str().map(|s| s.trim()) {
|
seen_te = true;
|
||||||
chunked = s.eq_ignore_ascii_case("chunked");
|
|
||||||
|
if let Ok(s) = value.to_str().map(str::trim) {
|
||||||
|
if s.eq_ignore_ascii_case("chunked") {
|
||||||
|
chunked = true;
|
||||||
|
} else if s.eq_ignore_ascii_case("identity") {
|
||||||
|
// allow silently since multiple TE headers are already checked
|
||||||
|
} else {
|
||||||
|
debug!("illegal Transfer-Encoding: {:?}", s);
|
||||||
|
return Err(ParseError::Header);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// connection keep-alive state
|
// connection keep-alive state
|
||||||
header::CONNECTION => {
|
header::CONNECTION => {
|
||||||
ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) {
|
ka = if let Ok(conn) = value.to_str().map(str::trim) {
|
||||||
if conn.eq_ignore_ascii_case("keep-alive") {
|
if conn.eq_ignore_ascii_case("keep-alive") {
|
||||||
Some(ConnectionType::KeepAlive)
|
Some(ConnectionType::KeepAlive)
|
||||||
} else if conn.eq_ignore_ascii_case("close") {
|
} else if conn.eq_ignore_ascii_case("close") {
|
||||||
@@ -125,7 +151,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
header::UPGRADE => {
|
header::UPGRADE => {
|
||||||
if let Ok(val) = value.to_str().map(|val| val.trim()) {
|
if let Ok(val) = value.to_str().map(str::trim) {
|
||||||
if val.eq_ignore_ascii_case("websocket") {
|
if val.eq_ignore_ascii_case("websocket") {
|
||||||
has_upgrade_websocket = true;
|
has_upgrade_websocket = true;
|
||||||
}
|
}
|
||||||
@@ -148,7 +174,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
self.set_expect()
|
self.set_expect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7230#section-3.3.3
|
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
|
||||||
if chunked {
|
if chunked {
|
||||||
// Chunked encoding
|
// Chunked encoding
|
||||||
Ok(PayloadLength::Payload(PayloadType::Payload(
|
Ok(PayloadLength::Payload(PayloadType::Payload(
|
||||||
@@ -186,10 +212,17 @@ impl MessageType for Request {
|
|||||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||||
|
|
||||||
let (len, method, uri, ver, h_len) = {
|
let (len, method, uri, ver, h_len) = {
|
||||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
// SAFETY:
|
||||||
|
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is
|
||||||
|
// safe because the type we are claiming to have initialized here is a
|
||||||
|
// bunch of `MaybeUninit`s, which do not require initialization.
|
||||||
|
let mut parsed = unsafe {
|
||||||
|
MaybeUninit::<[MaybeUninit<httparse::Header<'_>>; MAX_HEADERS]>::uninit()
|
||||||
|
.assume_init()
|
||||||
|
};
|
||||||
|
|
||||||
let mut req = httparse::Request::new(&mut parsed);
|
let mut req = httparse::Request::new(&mut []);
|
||||||
match req.parse(src)? {
|
match req.parse_with_uninit_headers(src, &mut parsed)? {
|
||||||
httparse::Status::Complete(len) => {
|
httparse::Status::Complete(len) => {
|
||||||
let method = Method::from_bytes(req.method.unwrap().as_bytes())
|
let method = Method::from_bytes(req.method.unwrap().as_bytes())
|
||||||
.map_err(|_| ParseError::Method)?;
|
.map_err(|_| ParseError::Method)?;
|
||||||
@@ -203,7 +236,15 @@ impl MessageType for Request {
|
|||||||
|
|
||||||
(len, method, uri, version, req.headers.len())
|
(len, method, uri, version, req.headers.len())
|
||||||
}
|
}
|
||||||
httparse::Status::Partial => return Ok(None),
|
httparse::Status::Partial => {
|
||||||
|
return if src.len() >= MAX_BUFFER_SIZE {
|
||||||
|
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||||
|
Err(ParseError::TooLarge)
|
||||||
|
} else {
|
||||||
|
// Return None to notify more read are needed for parsing request
|
||||||
|
Ok(None)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,15 +257,12 @@ impl MessageType for Request {
|
|||||||
let decoder = match length {
|
let decoder = match length {
|
||||||
PayloadLength::Payload(pl) => pl,
|
PayloadLength::Payload(pl) => pl,
|
||||||
PayloadLength::UpgradeWebSocket => {
|
PayloadLength::UpgradeWebSocket => {
|
||||||
// upgrade(websocket)
|
// upgrade (WebSocket)
|
||||||
PayloadType::Stream(PayloadDecoder::eof())
|
PayloadType::Stream(PayloadDecoder::eof())
|
||||||
}
|
}
|
||||||
PayloadLength::None => {
|
PayloadLength::None => {
|
||||||
if method == Method::CONNECT {
|
if method == Method::CONNECT {
|
||||||
PayloadType::Stream(PayloadDecoder::eof())
|
PayloadType::Stream(PayloadDecoder::eof())
|
||||||
} else if src.len() >= MAX_BUFFER_SIZE {
|
|
||||||
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
|
||||||
return Err(ParseError::TooLarge);
|
|
||||||
} else {
|
} else {
|
||||||
PayloadType::None
|
PayloadType::None
|
||||||
}
|
}
|
||||||
@@ -273,7 +311,14 @@ impl MessageType for ResponseHead {
|
|||||||
|
|
||||||
(len, version, status, res.headers.len())
|
(len, version, status, res.headers.len())
|
||||||
}
|
}
|
||||||
httparse::Status::Partial => return Ok(None),
|
httparse::Status::Partial => {
|
||||||
|
return if src.len() >= MAX_BUFFER_SIZE {
|
||||||
|
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||||
|
Err(ParseError::TooLarge)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,9 +334,6 @@ impl MessageType for ResponseHead {
|
|||||||
} else if status == StatusCode::SWITCHING_PROTOCOLS {
|
} else if status == StatusCode::SWITCHING_PROTOCOLS {
|
||||||
// switching protocol or connect
|
// switching protocol or connect
|
||||||
PayloadType::Stream(PayloadDecoder::eof())
|
PayloadType::Stream(PayloadDecoder::eof())
|
||||||
} else if src.len() >= MAX_BUFFER_SIZE {
|
|
||||||
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
|
||||||
return Err(ParseError::TooLarge);
|
|
||||||
} else {
|
} else {
|
||||||
// for HTTP/1.0 read to eof and close connection
|
// for HTTP/1.0 read to eof and close connection
|
||||||
if msg.version == Version::HTTP_10 {
|
if msg.version == Version::HTTP_10 {
|
||||||
@@ -399,20 +441,6 @@ enum Kind {
|
|||||||
Eof,
|
Eof,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
|
||||||
enum ChunkedState {
|
|
||||||
Size,
|
|
||||||
SizeLws,
|
|
||||||
Extension,
|
|
||||||
SizeLf,
|
|
||||||
Body,
|
|
||||||
BodyCr,
|
|
||||||
BodyLf,
|
|
||||||
EndCr,
|
|
||||||
EndLf,
|
|
||||||
End,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Decoder for PayloadDecoder {
|
impl Decoder for PayloadDecoder {
|
||||||
type Item = PayloadItem;
|
type Item = PayloadItem;
|
||||||
type Error = io::Error;
|
type Error = io::Error;
|
||||||
@@ -442,19 +470,23 @@ impl Decoder for PayloadDecoder {
|
|||||||
Kind::Chunked(ref mut state, ref mut size) => {
|
Kind::Chunked(ref mut state, ref mut size) => {
|
||||||
loop {
|
loop {
|
||||||
let mut buf = None;
|
let mut buf = None;
|
||||||
|
|
||||||
// advances the chunked state
|
// advances the chunked state
|
||||||
*state = match state.step(src, size, &mut buf) {
|
*state = match state.step(src, size, &mut buf) {
|
||||||
Poll::Pending => return Ok(None),
|
Poll::Pending => return Ok(None),
|
||||||
Poll::Ready(Ok(state)) => state,
|
Poll::Ready(Ok(state)) => state,
|
||||||
Poll::Ready(Err(e)) => return Err(e),
|
Poll::Ready(Err(e)) => return Err(e),
|
||||||
};
|
};
|
||||||
|
|
||||||
if *state == ChunkedState::End {
|
if *state == ChunkedState::End {
|
||||||
trace!("End of chunked stream");
|
trace!("End of chunked stream");
|
||||||
return Ok(Some(PayloadItem::Eof));
|
return Ok(Some(PayloadItem::Eof));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(buf) = buf {
|
if let Some(buf) = buf {
|
||||||
return Ok(Some(PayloadItem::Chunk(buf)));
|
return Ok(Some(PayloadItem::Chunk(buf)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if src.is_empty() {
|
if src.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -471,201 +503,40 @@ impl Decoder for PayloadDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! byte (
|
|
||||||
($rdr:ident) => ({
|
|
||||||
if $rdr.len() > 0 {
|
|
||||||
let b = $rdr[0];
|
|
||||||
$rdr.advance(1);
|
|
||||||
b
|
|
||||||
} else {
|
|
||||||
return Poll::Pending
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
impl ChunkedState {
|
|
||||||
fn step(
|
|
||||||
&self,
|
|
||||||
body: &mut BytesMut,
|
|
||||||
size: &mut u64,
|
|
||||||
buf: &mut Option<Bytes>,
|
|
||||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
use self::ChunkedState::*;
|
|
||||||
match *self {
|
|
||||||
Size => ChunkedState::read_size(body, size),
|
|
||||||
SizeLws => ChunkedState::read_size_lws(body),
|
|
||||||
Extension => ChunkedState::read_extension(body),
|
|
||||||
SizeLf => ChunkedState::read_size_lf(body, size),
|
|
||||||
Body => ChunkedState::read_body(body, size, buf),
|
|
||||||
BodyCr => ChunkedState::read_body_cr(body),
|
|
||||||
BodyLf => ChunkedState::read_body_lf(body),
|
|
||||||
EndCr => ChunkedState::read_end_cr(body),
|
|
||||||
EndLf => ChunkedState::read_end_lf(body),
|
|
||||||
End => Poll::Ready(Ok(ChunkedState::End)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_size(
|
|
||||||
rdr: &mut BytesMut,
|
|
||||||
size: &mut u64,
|
|
||||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
let radix = 16;
|
|
||||||
match byte!(rdr) {
|
|
||||||
b @ b'0'..=b'9' => {
|
|
||||||
*size *= radix;
|
|
||||||
*size += u64::from(b - b'0');
|
|
||||||
}
|
|
||||||
b @ b'a'..=b'f' => {
|
|
||||||
*size *= radix;
|
|
||||||
*size += u64::from(b + 10 - b'a');
|
|
||||||
}
|
|
||||||
b @ b'A'..=b'F' => {
|
|
||||||
*size *= radix;
|
|
||||||
*size += u64::from(b + 10 - b'A');
|
|
||||||
}
|
|
||||||
b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)),
|
|
||||||
b';' => return Poll::Ready(Ok(ChunkedState::Extension)),
|
|
||||||
b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)),
|
|
||||||
_ => {
|
|
||||||
return Poll::Ready(Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Invalid chunk size line: Invalid Size",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Poll::Ready(Ok(ChunkedState::Size))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_size_lws(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
trace!("read_size_lws");
|
|
||||||
match byte!(rdr) {
|
|
||||||
// LWS can follow the chunk size, but no more digits can come
|
|
||||||
b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)),
|
|
||||||
b';' => Poll::Ready(Ok(ChunkedState::Extension)),
|
|
||||||
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
|
|
||||||
_ => Poll::Ready(Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Invalid chunk size linear white space",
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn read_extension(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
match byte!(rdr) {
|
|
||||||
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
|
|
||||||
_ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn read_size_lf(
|
|
||||||
rdr: &mut BytesMut,
|
|
||||||
size: &mut u64,
|
|
||||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
match byte!(rdr) {
|
|
||||||
b'\n' if *size > 0 => Poll::Ready(Ok(ChunkedState::Body)),
|
|
||||||
b'\n' if *size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)),
|
|
||||||
_ => Poll::Ready(Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Invalid chunk size LF",
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_body(
|
|
||||||
rdr: &mut BytesMut,
|
|
||||||
rem: &mut u64,
|
|
||||||
buf: &mut Option<Bytes>,
|
|
||||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
trace!("Chunked read, remaining={:?}", rem);
|
|
||||||
|
|
||||||
let len = rdr.len() as u64;
|
|
||||||
if len == 0 {
|
|
||||||
Poll::Ready(Ok(ChunkedState::Body))
|
|
||||||
} else {
|
|
||||||
let slice;
|
|
||||||
if *rem > len {
|
|
||||||
slice = rdr.split().freeze();
|
|
||||||
*rem -= len;
|
|
||||||
} else {
|
|
||||||
slice = rdr.split_to(*rem as usize).freeze();
|
|
||||||
*rem = 0;
|
|
||||||
}
|
|
||||||
*buf = Some(slice);
|
|
||||||
if *rem > 0 {
|
|
||||||
Poll::Ready(Ok(ChunkedState::Body))
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Ok(ChunkedState::BodyCr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_body_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
match byte!(rdr) {
|
|
||||||
b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)),
|
|
||||||
_ => Poll::Ready(Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Invalid chunk body CR",
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn read_body_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
match byte!(rdr) {
|
|
||||||
b'\n' => Poll::Ready(Ok(ChunkedState::Size)),
|
|
||||||
_ => Poll::Ready(Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Invalid chunk body LF",
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn read_end_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
match byte!(rdr) {
|
|
||||||
b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)),
|
|
||||||
_ => Poll::Ready(Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Invalid chunk end CR",
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn read_end_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
|
|
||||||
match byte!(rdr) {
|
|
||||||
b'\n' => Poll::Ready(Ok(ChunkedState::End)),
|
|
||||||
_ => Poll::Ready(Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"Invalid chunk end LF",
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use http::{Method, Version};
|
use http::{Method, Version};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::error::ParseError;
|
use crate::{
|
||||||
use crate::http::header::{HeaderName, SET_COOKIE};
|
error::ParseError,
|
||||||
use crate::httpmessage::HttpMessage;
|
header::{HeaderName, SET_COOKIE},
|
||||||
|
HttpMessage as _,
|
||||||
|
};
|
||||||
|
|
||||||
impl PayloadType {
|
impl PayloadType {
|
||||||
fn unwrap(self) -> PayloadDecoder {
|
pub(crate) fn unwrap(self) -> PayloadDecoder {
|
||||||
match self {
|
match self {
|
||||||
PayloadType::Payload(pl) => pl,
|
PayloadType::Payload(pl) => pl,
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_unhandled(&self) -> bool {
|
pub(crate) fn is_unhandled(&self) -> bool {
|
||||||
matches!(self, PayloadType::Stream(_))
|
matches!(self, PayloadType::Stream(_))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PayloadItem {
|
impl PayloadItem {
|
||||||
fn chunk(self) -> Bytes {
|
pub(crate) fn chunk(self) -> Bytes {
|
||||||
match self {
|
match self {
|
||||||
PayloadItem::Chunk(chunk) => chunk,
|
PayloadItem::Chunk(chunk) => chunk,
|
||||||
_ => panic!("error"),
|
_ => panic!("error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn eof(&self) -> bool {
|
|
||||||
|
pub(crate) fn eof(&self) -> bool {
|
||||||
matches!(*self, PayloadItem::Eof)
|
matches!(*self, PayloadItem::Eof)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -821,8 +692,8 @@ mod tests {
|
|||||||
.get_all(SET_COOKIE)
|
.get_all(SET_COOKIE)
|
||||||
.map(|v| v.to_str().unwrap().to_owned())
|
.map(|v| v.to_str().unwrap().to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(val[1], "c1=cookie1");
|
assert_eq!(val[0], "c1=cookie1");
|
||||||
assert_eq!(val[0], "c2=cookie2");
|
assert_eq!(val[1], "c2=cookie2");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -958,34 +829,6 @@ mod tests {
|
|||||||
assert!(req.upgrade());
|
assert!(req.upgrade());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_request_chunked() {
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"GET /test HTTP/1.1\r\n\
|
|
||||||
transfer-encoding: chunked\r\n\r\n",
|
|
||||||
);
|
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
if let Ok(val) = req.chunked() {
|
|
||||||
assert!(val);
|
|
||||||
} else {
|
|
||||||
unreachable!("Error");
|
|
||||||
}
|
|
||||||
|
|
||||||
// intentional typo in "chunked"
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"GET /test HTTP/1.1\r\n\
|
|
||||||
transfer-encoding: chnked\r\n\r\n",
|
|
||||||
);
|
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
if let Ok(val) = req.chunked() {
|
|
||||||
assert!(!val);
|
|
||||||
} else {
|
|
||||||
unreachable!("Error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_headers_content_length_err_1() {
|
fn test_headers_content_length_err_1() {
|
||||||
let mut buf = BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
@@ -1103,128 +946,9 @@ mod tests {
|
|||||||
expect_parse_err!(&mut buf);
|
expect_parse_err!(&mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_http_request_chunked_payload() {
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"GET /test HTTP/1.1\r\n\
|
|
||||||
transfer-encoding: chunked\r\n\r\n",
|
|
||||||
);
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
|
||||||
let mut pl = pl.unwrap();
|
|
||||||
assert!(req.chunked().unwrap());
|
|
||||||
|
|
||||||
buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n");
|
|
||||||
assert_eq!(
|
|
||||||
pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(),
|
|
||||||
b"data"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(),
|
|
||||||
b"line"
|
|
||||||
);
|
|
||||||
assert!(pl.decode(&mut buf).unwrap().unwrap().eof());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_http_request_chunked_payload_and_next_message() {
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"GET /test HTTP/1.1\r\n\
|
|
||||||
transfer-encoding: chunked\r\n\r\n",
|
|
||||||
);
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
|
||||||
let mut pl = pl.unwrap();
|
|
||||||
assert!(req.chunked().unwrap());
|
|
||||||
|
|
||||||
buf.extend(
|
|
||||||
b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\
|
|
||||||
POST /test2 HTTP/1.1\r\n\
|
|
||||||
transfer-encoding: chunked\r\n\r\n"
|
|
||||||
.iter(),
|
|
||||||
);
|
|
||||||
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(msg.chunk().as_ref(), b"data");
|
|
||||||
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(msg.chunk().as_ref(), b"line");
|
|
||||||
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert!(msg.eof());
|
|
||||||
|
|
||||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert!(req.chunked().unwrap());
|
|
||||||
assert_eq!(*req.method(), Method::POST);
|
|
||||||
assert!(req.chunked().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_http_request_chunked_payload_chunks() {
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"GET /test HTTP/1.1\r\n\
|
|
||||||
transfer-encoding: chunked\r\n\r\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
|
||||||
let mut pl = pl.unwrap();
|
|
||||||
assert!(req.chunked().unwrap());
|
|
||||||
|
|
||||||
buf.extend(b"4\r\n1111\r\n");
|
|
||||||
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(msg.chunk().as_ref(), b"1111");
|
|
||||||
|
|
||||||
buf.extend(b"4\r\ndata\r");
|
|
||||||
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(msg.chunk().as_ref(), b"data");
|
|
||||||
|
|
||||||
buf.extend(b"\n4");
|
|
||||||
assert!(pl.decode(&mut buf).unwrap().is_none());
|
|
||||||
|
|
||||||
buf.extend(b"\r");
|
|
||||||
assert!(pl.decode(&mut buf).unwrap().is_none());
|
|
||||||
buf.extend(b"\n");
|
|
||||||
assert!(pl.decode(&mut buf).unwrap().is_none());
|
|
||||||
|
|
||||||
buf.extend(b"li");
|
|
||||||
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(msg.chunk().as_ref(), b"li");
|
|
||||||
|
|
||||||
//trailers
|
|
||||||
//buf.feed_data("test: test\r\n");
|
|
||||||
//not_ready!(reader.parse(&mut buf, &mut readbuf));
|
|
||||||
|
|
||||||
buf.extend(b"ne\r\n0\r\n");
|
|
||||||
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(msg.chunk().as_ref(), b"ne");
|
|
||||||
assert!(pl.decode(&mut buf).unwrap().is_none());
|
|
||||||
|
|
||||||
buf.extend(b"\r\n");
|
|
||||||
assert!(pl.decode(&mut buf).unwrap().unwrap().eof());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_chunked_payload_chunk_extension() {
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
&"GET /test HTTP/1.1\r\n\
|
|
||||||
transfer-encoding: chunked\r\n\r\n"[..],
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
|
||||||
let mut pl = pl.unwrap();
|
|
||||||
assert!(msg.chunked().unwrap());
|
|
||||||
|
|
||||||
buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n")
|
|
||||||
let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk();
|
|
||||||
assert_eq!(chunk, Bytes::from_static(b"data"));
|
|
||||||
let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk();
|
|
||||||
assert_eq!(chunk, Bytes::from_static(b"line"));
|
|
||||||
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert!(msg.eof());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_http10_read_until_eof() {
|
fn test_response_http10_read_until_eof() {
|
||||||
let mut buf = BytesMut::from(&"HTTP/1.0 200 Ok\r\n\r\ntest data"[..]);
|
let mut buf = BytesMut::from("HTTP/1.0 200 Ok\r\n\r\ntest data");
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<ResponseHead>::default();
|
let mut reader = MessageDecoder::<ResponseHead>::default();
|
||||||
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
@@ -1233,4 +957,84 @@ mod tests {
|
|||||||
let chunk = pl.decode(&mut buf).unwrap().unwrap();
|
let chunk = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"test data")));
|
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"test data")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hrs_multiple_content_length() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET / HTTP/1.1\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Content-Length: 4\r\n\
|
||||||
|
Content-Length: 2\r\n\
|
||||||
|
\r\n\
|
||||||
|
abcd",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hrs_content_length_plus() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET / HTTP/1.1\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Content-Length: +3\r\n\
|
||||||
|
\r\n\
|
||||||
|
000",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hrs_unknown_transfer_encoding() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET / HTTP/1.1\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Transfer-Encoding: JUNK\r\n\
|
||||||
|
Transfer-Encoding: chunked\r\n\
|
||||||
|
\r\n\
|
||||||
|
5\r\n\
|
||||||
|
hello\r\n\
|
||||||
|
0",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hrs_multiple_transfer_encoding() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET / HTTP/1.1\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Content-Length: 51\r\n\
|
||||||
|
Transfer-Encoding: identity\r\n\
|
||||||
|
Transfer-Encoding: chunked\r\n\
|
||||||
|
\r\n\
|
||||||
|
0\r\n\
|
||||||
|
\r\n\
|
||||||
|
GET /forbidden HTTP/1.1\r\n\
|
||||||
|
Host: example.com\r\n\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transfer_encoding_agrees() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test HTTP/1.1\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Content-Length: 3\r\n\
|
||||||
|
Transfer-Encoding: identity\r\n\
|
||||||
|
\r\n\
|
||||||
|
0\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
let mut pl = pl.unwrap();
|
||||||
|
|
||||||
|
let chunk = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"0\r\n")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,21 @@ use std::{cmp, io};
|
|||||||
|
|
||||||
use bytes::{BufMut, BytesMut};
|
use bytes::{BufMut, BytesMut};
|
||||||
|
|
||||||
use crate::body::BodySize;
|
use crate::{
|
||||||
use crate::config::ServiceConfig;
|
body::BodySize,
|
||||||
use crate::header::map;
|
config::ServiceConfig,
|
||||||
use crate::helpers;
|
header::{map::Value, HeaderMap, HeaderName},
|
||||||
use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
|
header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING},
|
||||||
use crate::http::{HeaderMap, StatusCode, Version};
|
helpers,
|
||||||
use crate::message::{ConnectionType, RequestHeadType};
|
message::{ConnectionType, RequestHeadType},
|
||||||
use crate::response::Response;
|
Response, StatusCode, Version,
|
||||||
|
};
|
||||||
|
|
||||||
const AVERAGE_HEADER_SIZE: usize = 30;
|
const AVERAGE_HEADER_SIZE: usize = 30;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct MessageEncoder<T: MessageType> {
|
pub(crate) struct MessageEncoder<T: MessageType> {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub length: BodySize,
|
pub length: BodySize,
|
||||||
pub te: TransferEncoding,
|
pub te: TransferEncoding,
|
||||||
_phantom: PhantomData<T>,
|
_phantom: PhantomData<T>,
|
||||||
@@ -54,7 +56,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
dst: &mut BytesMut,
|
dst: &mut BytesMut,
|
||||||
version: Version,
|
version: Version,
|
||||||
mut length: BodySize,
|
mut length: BodySize,
|
||||||
ctype: ConnectionType,
|
conn_type: ConnectionType,
|
||||||
config: &ServiceConfig,
|
config: &ServiceConfig,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let chunked = self.chunked();
|
let chunked = self.chunked();
|
||||||
@@ -69,17 +71,28 @@ pub(crate) trait MessageType: Sized {
|
|||||||
| StatusCode::PROCESSING
|
| StatusCode::PROCESSING
|
||||||
| StatusCode::NO_CONTENT => {
|
| StatusCode::NO_CONTENT => {
|
||||||
// skip content-length and transfer-encoding headers
|
// skip content-length and transfer-encoding headers
|
||||||
// See https://tools.ietf.org/html/rfc7230#section-3.3.1
|
// see https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1
|
||||||
// and https://tools.ietf.org/html/rfc7230#section-3.3.2
|
// and https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
|
||||||
skip_len = true;
|
skip_len = true;
|
||||||
length = BodySize::None
|
length = BodySize::None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusCode::NOT_MODIFIED => {
|
||||||
|
// 304 responses should never have a body but should retain a manually set
|
||||||
|
// content-length header
|
||||||
|
// see https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
|
||||||
|
skip_len = false;
|
||||||
|
length = BodySize::None;
|
||||||
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match length {
|
match length {
|
||||||
BodySize::Stream => {
|
BodySize::Stream => {
|
||||||
if chunked {
|
if chunked {
|
||||||
|
skip_len = true;
|
||||||
if camel_case {
|
if camel_case {
|
||||||
dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n")
|
dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -90,19 +103,16 @@ pub(crate) trait MessageType: Sized {
|
|||||||
dst.put_slice(b"\r\n");
|
dst.put_slice(b"\r\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BodySize::Empty => {
|
BodySize::Sized(0) if camel_case => {
|
||||||
if camel_case {
|
dst.put_slice(b"\r\nContent-Length: 0\r\n")
|
||||||
dst.put_slice(b"\r\nContent-Length: 0\r\n");
|
|
||||||
} else {
|
|
||||||
dst.put_slice(b"\r\ncontent-length: 0\r\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"),
|
||||||
BodySize::Sized(len) => helpers::write_content_length(len, dst),
|
BodySize::Sized(len) => helpers::write_content_length(len, dst),
|
||||||
BodySize::None => dst.put_slice(b"\r\n"),
|
BodySize::None => dst.put_slice(b"\r\n"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection
|
// Connection
|
||||||
match ctype {
|
match conn_type {
|
||||||
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
|
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
|
||||||
ConnectionType::KeepAlive if version < Version::HTTP_11 => {
|
ConnectionType::KeepAlive if version < Version::HTTP_11 => {
|
||||||
if camel_case {
|
if camel_case {
|
||||||
@@ -121,21 +131,11 @@ pub(crate) trait MessageType: Sized {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// merging headers from head and extra headers. HeaderMap::new() does not allocate.
|
|
||||||
let empty_headers = HeaderMap::new();
|
|
||||||
let extra_headers = self.extra_headers().unwrap_or(&empty_headers);
|
|
||||||
let headers = self
|
|
||||||
.headers()
|
|
||||||
.inner
|
|
||||||
.iter()
|
|
||||||
.filter(|(name, _)| !extra_headers.contains_key(*name))
|
|
||||||
.chain(extra_headers.inner.iter());
|
|
||||||
|
|
||||||
// write headers
|
// write headers
|
||||||
|
|
||||||
let mut has_date = false;
|
let mut has_date = false;
|
||||||
|
|
||||||
let mut buf = dst.chunk_mut().as_mut_ptr() as *mut u8;
|
let mut buf = dst.chunk_mut().as_mut_ptr();
|
||||||
let mut remaining = dst.capacity() - dst.len();
|
let mut remaining = dst.capacity() - dst.len();
|
||||||
|
|
||||||
// tracks bytes written since last buffer resize
|
// tracks bytes written since last buffer resize
|
||||||
@@ -143,10 +143,10 @@ pub(crate) trait MessageType: Sized {
|
|||||||
// container's knowledge, this is used to sync the containers cursor after data is written
|
// container's knowledge, this is used to sync the containers cursor after data is written
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
|
|
||||||
for (key, value) in headers {
|
self.write_headers(|key, value| {
|
||||||
match *key {
|
match *key {
|
||||||
CONNECTION => continue,
|
CONNECTION => return,
|
||||||
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue,
|
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => return,
|
||||||
DATE => has_date = true,
|
DATE => has_date = true,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -154,106 +154,56 @@ pub(crate) trait MessageType: Sized {
|
|||||||
let k = key.as_str().as_bytes();
|
let k = key.as_str().as_bytes();
|
||||||
let k_len = k.len();
|
let k_len = k.len();
|
||||||
|
|
||||||
match value {
|
// TODO: drain?
|
||||||
map::Value::One(ref val) => {
|
for val in value.iter() {
|
||||||
let v = val.as_ref();
|
let v = val.as_ref();
|
||||||
let v_len = v.len();
|
let v_len = v.len();
|
||||||
|
|
||||||
// key length + value length + colon + space + \r\n
|
// key length + value length + colon + space + \r\n
|
||||||
let len = k_len + v_len + 4;
|
let len = k_len + v_len + 4;
|
||||||
|
|
||||||
if len > remaining {
|
if len > remaining {
|
||||||
// not enough room in buffer for this header; reserve more space
|
// SAFETY: all the bytes written up to position "pos" are initialized
|
||||||
|
// the written byte count and pointer advancement are kept in sync
|
||||||
// SAFETY: all the bytes written up to position "pos" are initialized
|
|
||||||
// the written byte count and pointer advancement are kept in sync
|
|
||||||
unsafe {
|
|
||||||
dst.advance_mut(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
pos = 0;
|
|
||||||
dst.reserve(len * 2);
|
|
||||||
remaining = dst.capacity() - dst.len();
|
|
||||||
|
|
||||||
// re-assign buf raw pointer since it's possible that the buffer was
|
|
||||||
// reallocated and/or resized
|
|
||||||
buf = dst.chunk_mut().as_mut_ptr() as *mut u8;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: on each write, it is enough to ensure that the advancement of the
|
|
||||||
// cursor matches the number of bytes written
|
|
||||||
unsafe {
|
unsafe {
|
||||||
// use upper Camel-Case
|
dst.advance_mut(pos);
|
||||||
if camel_case {
|
|
||||||
write_camel_case(k, from_raw_parts_mut(buf, k_len))
|
|
||||||
} else {
|
|
||||||
write_data(k, buf, k_len)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf = buf.add(k_len);
|
|
||||||
|
|
||||||
write_data(b": ", buf, 2);
|
|
||||||
buf = buf.add(2);
|
|
||||||
|
|
||||||
write_data(v, buf, v_len);
|
|
||||||
buf = buf.add(v_len);
|
|
||||||
|
|
||||||
write_data(b"\r\n", buf, 2);
|
|
||||||
buf = buf.add(2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pos += len;
|
pos = 0;
|
||||||
remaining -= len;
|
dst.reserve(len * 2);
|
||||||
|
remaining = dst.capacity() - dst.len();
|
||||||
|
|
||||||
|
// re-assign buf raw pointer since it's possible that the buffer was
|
||||||
|
// reallocated and/or resized
|
||||||
|
buf = dst.chunk_mut().as_mut_ptr();
|
||||||
}
|
}
|
||||||
|
|
||||||
map::Value::Multi(ref vec) => {
|
// SAFETY: on each write, it is enough to ensure that the advancement of
|
||||||
for val in vec {
|
// the cursor matches the number of bytes written
|
||||||
let v = val.as_ref();
|
unsafe {
|
||||||
let v_len = v.len();
|
if camel_case {
|
||||||
let len = k_len + v_len + 4;
|
// use Camel-Case headers
|
||||||
|
write_camel_case(k, buf, k_len);
|
||||||
if len > remaining {
|
} else {
|
||||||
// SAFETY: all the bytes written up to position "pos" are initialized
|
write_data(k, buf, k_len);
|
||||||
// the written byte count and pointer advancement are kept in sync
|
|
||||||
unsafe {
|
|
||||||
dst.advance_mut(pos);
|
|
||||||
}
|
|
||||||
pos = 0;
|
|
||||||
dst.reserve(len * 2);
|
|
||||||
remaining = dst.capacity() - dst.len();
|
|
||||||
|
|
||||||
// re-assign buf raw pointer since it's possible that the buffer was
|
|
||||||
// reallocated and/or resized
|
|
||||||
buf = dst.chunk_mut().as_mut_ptr() as *mut u8;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: on each write, it is enough to ensure that the advancement of
|
|
||||||
// the cursor matches the number of bytes written
|
|
||||||
unsafe {
|
|
||||||
if camel_case {
|
|
||||||
write_camel_case(k, from_raw_parts_mut(buf, k_len));
|
|
||||||
} else {
|
|
||||||
write_data(k, buf, k_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
buf = buf.add(k_len);
|
|
||||||
|
|
||||||
write_data(b": ", buf, 2);
|
|
||||||
buf = buf.add(2);
|
|
||||||
|
|
||||||
write_data(v, buf, v_len);
|
|
||||||
buf = buf.add(v_len);
|
|
||||||
|
|
||||||
write_data(b"\r\n", buf, 2);
|
|
||||||
buf = buf.add(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
pos += len;
|
|
||||||
remaining -= len;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
buf = buf.add(k_len);
|
||||||
|
|
||||||
|
write_data(b": ", buf, 2);
|
||||||
|
buf = buf.add(2);
|
||||||
|
|
||||||
|
write_data(v, buf, v_len);
|
||||||
|
buf = buf.add(v_len);
|
||||||
|
|
||||||
|
write_data(b"\r\n", buf, 2);
|
||||||
|
buf = buf.add(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
pos += len;
|
||||||
|
remaining -= len;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// final cursor synchronization with the bytes container
|
// final cursor synchronization with the bytes container
|
||||||
//
|
//
|
||||||
@@ -273,6 +223,24 @@ pub(crate) trait MessageType: Sized {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_headers<F>(&mut self, mut f: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&HeaderName, &Value),
|
||||||
|
{
|
||||||
|
match self.extra_headers() {
|
||||||
|
Some(headers) => {
|
||||||
|
// merging headers from head and extra headers.
|
||||||
|
self.headers()
|
||||||
|
.inner
|
||||||
|
.iter()
|
||||||
|
.filter(|(name, _)| !headers.contains_key(*name))
|
||||||
|
.chain(headers.inner.iter())
|
||||||
|
.for_each(|(k, v)| f(k, v))
|
||||||
|
}
|
||||||
|
None => self.headers().inner.iter().for_each(|(k, v)| f(k, v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageType for Response<()> {
|
impl MessageType for Response<()> {
|
||||||
@@ -329,7 +297,7 @@ impl MessageType for RequestHeadType {
|
|||||||
let head = self.as_ref();
|
let head = self.as_ref();
|
||||||
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
|
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
|
||||||
write!(
|
write!(
|
||||||
Writer(dst),
|
helpers::MutWriter(dst),
|
||||||
"{} {} {}",
|
"{} {} {}",
|
||||||
head.method,
|
head.method,
|
||||||
head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
|
head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
|
||||||
@@ -369,13 +337,13 @@ impl<T: MessageType> MessageEncoder<T> {
|
|||||||
stream: bool,
|
stream: bool,
|
||||||
version: Version,
|
version: Version,
|
||||||
length: BodySize,
|
length: BodySize,
|
||||||
ctype: ConnectionType,
|
conn_type: ConnectionType,
|
||||||
config: &ServiceConfig,
|
config: &ServiceConfig,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
// transfer encoding
|
// transfer encoding
|
||||||
if !head {
|
if !head {
|
||||||
self.te = match length {
|
self.te = match length {
|
||||||
BodySize::Empty => TransferEncoding::empty(),
|
BodySize::Sized(0) => TransferEncoding::empty(),
|
||||||
BodySize::Sized(len) => TransferEncoding::length(len),
|
BodySize::Sized(len) => TransferEncoding::length(len),
|
||||||
BodySize::Stream => {
|
BodySize::Stream => {
|
||||||
if message.chunked() && !stream {
|
if message.chunked() && !stream {
|
||||||
@@ -391,7 +359,7 @@ impl<T: MessageType> MessageEncoder<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message.encode_status(dst)?;
|
message.encode_status(dst)?;
|
||||||
message.encode_headers(dst, version, length, ctype, config)
|
message.encode_headers(dst, version, length, conn_type, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,10 +373,12 @@ pub(crate) struct TransferEncoding {
|
|||||||
enum TransferEncodingKind {
|
enum TransferEncodingKind {
|
||||||
/// An Encoder for when Transfer-Encoding includes `chunked`.
|
/// An Encoder for when Transfer-Encoding includes `chunked`.
|
||||||
Chunked(bool),
|
Chunked(bool),
|
||||||
|
|
||||||
/// An Encoder for when Content-Length is set.
|
/// An Encoder for when Content-Length is set.
|
||||||
///
|
///
|
||||||
/// Enforces that the body is not longer than the Content-Length header.
|
/// Enforces that the body is not longer than the Content-Length header.
|
||||||
Length(u64),
|
Length(u64),
|
||||||
|
|
||||||
/// An Encoder for when Content-Length is not known.
|
/// An Encoder for when Content-Length is not known.
|
||||||
///
|
///
|
||||||
/// Application decides when to stop writing.
|
/// Application decides when to stop writing.
|
||||||
@@ -462,7 +432,7 @@ impl TransferEncoding {
|
|||||||
*eof = true;
|
*eof = true;
|
||||||
buf.extend_from_slice(b"0\r\n\r\n");
|
buf.extend_from_slice(b"0\r\n\r\n");
|
||||||
} else {
|
} else {
|
||||||
writeln!(Writer(buf), "{:X}\r", msg.len())
|
writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len())
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||||
|
|
||||||
buf.reserve(msg.len() + 2);
|
buf.reserve(msg.len() + 2);
|
||||||
@@ -512,28 +482,23 @@ impl TransferEncoding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Writer<'a>(pub &'a mut BytesMut);
|
|
||||||
|
|
||||||
impl<'a> io::Write for Writer<'a> {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
self.0.extend_from_slice(buf);
|
|
||||||
Ok(buf.len())
|
|
||||||
}
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// Callers must ensure that the given length matches given value length.
|
/// Callers must ensure that the given `len` matches the given `value` length and that `buf` is
|
||||||
|
/// valid for writes of at least `len` bytes.
|
||||||
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
|
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
|
||||||
debug_assert_eq!(value.len(), len);
|
debug_assert_eq!(value.len(), len);
|
||||||
copy_nonoverlapping(value.as_ptr(), buf, len);
|
copy_nonoverlapping(value.as_ptr(), buf, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
/// # Safety
|
||||||
|
/// Callers must ensure that the given `len` matches the given `value` length and that `buf` is
|
||||||
|
/// valid for writes of at least `len` bytes.
|
||||||
|
unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
|
||||||
// first copy entire (potentially wrong) slice to output
|
// first copy entire (potentially wrong) slice to output
|
||||||
buffer[..value.len()].copy_from_slice(value);
|
write_data(value, buf, len);
|
||||||
|
|
||||||
|
// SAFETY: We just initialized the buffer with `value`
|
||||||
|
let buffer = from_raw_parts_mut(buf, len);
|
||||||
|
|
||||||
let mut iter = value.iter();
|
let mut iter = value.iter();
|
||||||
|
|
||||||
@@ -566,8 +531,10 @@ mod tests {
|
|||||||
use http::header::AUTHORIZATION;
|
use http::header::AUTHORIZATION;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::http::header::{HeaderValue, CONTENT_TYPE};
|
use crate::{
|
||||||
use crate::RequestHead;
|
header::{HeaderValue, CONTENT_TYPE},
|
||||||
|
RequestHead,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_chunked_te() {
|
fn test_chunked_te() {
|
||||||
@@ -583,8 +550,8 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[actix_rt::test]
|
||||||
fn test_camel_case() {
|
async fn test_camel_case() {
|
||||||
let mut bytes = BytesMut::with_capacity(2048);
|
let mut bytes = BytesMut::with_capacity(2048);
|
||||||
let mut head = RequestHead::default();
|
let mut head = RequestHead::default();
|
||||||
head.set_camel_case_headers(true);
|
head.set_camel_case_headers(true);
|
||||||
@@ -597,13 +564,12 @@ mod tests {
|
|||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
&mut bytes,
|
&mut bytes,
|
||||||
Version::HTTP_11,
|
Version::HTTP_11,
|
||||||
BodySize::Empty,
|
BodySize::Sized(0),
|
||||||
ConnectionType::Close,
|
ConnectionType::Close,
|
||||||
&ServiceConfig::default(),
|
&ServiceConfig::default(),
|
||||||
);
|
);
|
||||||
let data =
|
let data =
|
||||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||||
eprintln!("{}", &data);
|
|
||||||
|
|
||||||
assert!(data.contains("Content-Length: 0\r\n"));
|
assert!(data.contains("Content-Length: 0\r\n"));
|
||||||
assert!(data.contains("Connection: close\r\n"));
|
assert!(data.contains("Connection: close\r\n"));
|
||||||
@@ -647,8 +613,8 @@ mod tests {
|
|||||||
assert!(data.contains("date: date\r\n"));
|
assert!(data.contains("date: date\r\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[actix_rt::test]
|
||||||
fn test_extra_headers() {
|
async fn test_extra_headers() {
|
||||||
let mut bytes = BytesMut::with_capacity(2048);
|
let mut bytes = BytesMut::with_capacity(2048);
|
||||||
|
|
||||||
let mut head = RequestHead::default();
|
let mut head = RequestHead::default();
|
||||||
@@ -669,7 +635,7 @@ mod tests {
|
|||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
&mut bytes,
|
&mut bytes,
|
||||||
Version::HTTP_11,
|
Version::HTTP_11,
|
||||||
BodySize::Empty,
|
BodySize::Sized(0),
|
||||||
ConnectionType::Close,
|
ConnectionType::Close,
|
||||||
&ServiceConfig::default(),
|
&ServiceConfig::default(),
|
||||||
);
|
);
|
||||||
@@ -681,16 +647,14 @@ mod tests {
|
|||||||
assert!(data.contains("date: date\r\n"));
|
assert!(data.contains("date: date\r\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[actix_rt::test]
|
||||||
fn test_no_content_length() {
|
async fn test_no_content_length() {
|
||||||
let mut bytes = BytesMut::with_capacity(2048);
|
let mut bytes = BytesMut::with_capacity(2048);
|
||||||
|
|
||||||
let mut res: Response<()> =
|
let mut res = Response::with_body(StatusCode::SWITCHING_PROTOCOLS, ());
|
||||||
Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>();
|
res.headers_mut().insert(DATE, HeaderValue::from_static(""));
|
||||||
res.headers_mut()
|
res.headers_mut()
|
||||||
.insert(DATE, HeaderValue::from_static(&""));
|
.insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
|
||||||
res.headers_mut()
|
|
||||||
.insert(CONTENT_LENGTH, HeaderValue::from_static(&"0"));
|
|
||||||
|
|
||||||
let _ = res.encode_headers(
|
let _ = res.encode_headers(
|
||||||
&mut bytes,
|
&mut bytes,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use std::task::{Context, Poll};
|
|
||||||
|
|
||||||
use actix_service::{Service, ServiceFactory};
|
use actix_service::{Service, ServiceFactory};
|
||||||
use futures_util::future::{ready, Ready};
|
use actix_utils::future::{ready, Ready};
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::request::Request;
|
use crate::request::Request;
|
||||||
@@ -26,11 +24,9 @@ impl Service<Request> for ExpectHandler {
|
|||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
actix_service::always_ready!();
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(&mut self, req: Request) -> Self::Future {
|
fn call(&self, req: Request) -> Self::Future {
|
||||||
ready(Ok(req))
|
ready(Ok(req))
|
||||||
// TODO: add some way to trigger error
|
// TODO: add some way to trigger error
|
||||||
// Err(error::ErrorExpectationFailed("test"))
|
// Err(error::ErrorExpectationFailed("test"))
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//! HTTP/1 implementation
|
//! HTTP/1 protocol implementation.
|
||||||
|
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
|
|
||||||
|
mod chunked;
|
||||||
mod client;
|
mod client;
|
||||||
mod codec;
|
mod codec;
|
||||||
mod decoder;
|
mod decoder;
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ use std::cell::RefCell;
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::rc::{Rc, Weak};
|
use std::rc::{Rc, Weak};
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll, Waker};
|
||||||
|
|
||||||
use actix_utils::task::LocalWaker;
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
|
|
||||||
@@ -134,7 +133,7 @@ impl PayloadSender {
|
|||||||
if shared.borrow().need_read {
|
if shared.borrow().need_read {
|
||||||
PayloadStatus::Read
|
PayloadStatus::Read
|
||||||
} else {
|
} else {
|
||||||
shared.borrow_mut().io_task.register(cx.waker());
|
shared.borrow_mut().register_io(cx);
|
||||||
PayloadStatus::Pause
|
PayloadStatus::Pause
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -150,8 +149,8 @@ struct Inner {
|
|||||||
err: Option<PayloadError>,
|
err: Option<PayloadError>,
|
||||||
need_read: bool,
|
need_read: bool,
|
||||||
items: VecDeque<Bytes>,
|
items: VecDeque<Bytes>,
|
||||||
task: LocalWaker,
|
task: Option<Waker>,
|
||||||
io_task: LocalWaker,
|
io_task: Option<Waker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Inner {
|
impl Inner {
|
||||||
@@ -162,8 +161,46 @@ impl Inner {
|
|||||||
err: None,
|
err: None,
|
||||||
items: VecDeque::new(),
|
items: VecDeque::new(),
|
||||||
need_read: true,
|
need_read: true,
|
||||||
task: LocalWaker::new(),
|
task: None,
|
||||||
io_task: LocalWaker::new(),
|
io_task: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wake up future waiting for payload data to be available.
|
||||||
|
fn wake(&mut self) {
|
||||||
|
if let Some(waker) = self.task.take() {
|
||||||
|
waker.wake();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wake up future feeding data to Payload.
|
||||||
|
fn wake_io(&mut self) {
|
||||||
|
if let Some(waker) = self.io_task.take() {
|
||||||
|
waker.wake();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register future waiting data from payload.
|
||||||
|
/// Waker would be used in `Inner::wake`
|
||||||
|
fn register(&mut self, cx: &mut Context<'_>) {
|
||||||
|
if self
|
||||||
|
.task
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |w| !cx.waker().will_wake(w))
|
||||||
|
{
|
||||||
|
self.task = Some(cx.waker().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register future feeding data to payload.
|
||||||
|
/// Waker would be used in `Inner::wake_io`
|
||||||
|
fn register_io(&mut self, cx: &mut Context<'_>) {
|
||||||
|
if self
|
||||||
|
.io_task
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |w| !cx.waker().will_wake(w))
|
||||||
|
{
|
||||||
|
self.io_task = Some(cx.waker().clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +219,7 @@ impl Inner {
|
|||||||
self.len += data.len();
|
self.len += data.len();
|
||||||
self.items.push_back(data);
|
self.items.push_back(data);
|
||||||
self.need_read = self.len < MAX_BUFFER_SIZE;
|
self.need_read = self.len < MAX_BUFFER_SIZE;
|
||||||
self.task.wake();
|
self.wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -199,9 +236,9 @@ impl Inner {
|
|||||||
self.need_read = self.len < MAX_BUFFER_SIZE;
|
self.need_read = self.len < MAX_BUFFER_SIZE;
|
||||||
|
|
||||||
if self.need_read && !self.eof {
|
if self.need_read && !self.eof {
|
||||||
self.task.register(cx.waker());
|
self.register(cx);
|
||||||
}
|
}
|
||||||
self.io_task.wake();
|
self.wake_io();
|
||||||
Poll::Ready(Some(Ok(data)))
|
Poll::Ready(Some(Ok(data)))
|
||||||
} else if let Some(err) = self.err.take() {
|
} else if let Some(err) = self.err.take() {
|
||||||
Poll::Ready(Some(Err(err)))
|
Poll::Ready(Some(Err(err)))
|
||||||
@@ -209,8 +246,8 @@ impl Inner {
|
|||||||
Poll::Ready(None)
|
Poll::Ready(None)
|
||||||
} else {
|
} else {
|
||||||
self.need_read = true;
|
self.need_read = true;
|
||||||
self.task.register(cx.waker());
|
self.register(cx);
|
||||||
self.io_task.wake();
|
self.wake_io();
|
||||||
Poll::Pending
|
Poll::Pending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,7 +261,7 @@ impl Inner {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use futures_util::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_unread_data() {
|
async fn test_unread_data() {
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
use std::cell::RefCell;
|
use std::{
|
||||||
use std::future::Future;
|
fmt,
|
||||||
use std::marker::PhantomData;
|
marker::PhantomData,
|
||||||
use std::pin::Pin;
|
net,
|
||||||
use std::rc::Rc;
|
rc::Rc,
|
||||||
use std::task::{Context, Poll};
|
task::{Context, Poll},
|
||||||
use std::{fmt, net};
|
};
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||||
use actix_rt::net::TcpStream;
|
use actix_rt::net::TcpStream;
|
||||||
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory};
|
use actix_service::{
|
||||||
use futures_core::ready;
|
fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
|
||||||
use futures_util::future::ready;
|
};
|
||||||
|
use actix_utils::future::ready;
|
||||||
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
use crate::body::MessageBody;
|
use crate::{
|
||||||
use crate::config::ServiceConfig;
|
body::{BoxBody, MessageBody},
|
||||||
use crate::error::{DispatchError, Error};
|
config::ServiceConfig,
|
||||||
use crate::request::Request;
|
error::DispatchError,
|
||||||
use crate::response::Response;
|
service::HttpServiceHandler,
|
||||||
use crate::service::HttpFlow;
|
ConnectCallback, OnConnectData, Request, Response,
|
||||||
use crate::{ConnectCallback, OnConnectData};
|
};
|
||||||
|
|
||||||
use super::codec::Codec;
|
use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler};
|
||||||
use super::dispatcher::Dispatcher;
|
|
||||||
use super::{ExpectHandler, UpgradeHandler};
|
|
||||||
|
|
||||||
/// `ServiceFactory` implementation for HTTP1 transport
|
/// `ServiceFactory` implementation for HTTP1 transport
|
||||||
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
|
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
|
||||||
@@ -37,7 +37,7 @@ pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
|
|||||||
impl<T, S, B> H1Service<T, S, B>
|
impl<T, S, B> H1Service<T, S, B>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
@@ -61,15 +61,21 @@ where
|
|||||||
impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
|
impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error>,
|
S::Future: 'static,
|
||||||
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
|
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Error: Into<Error>,
|
X::Future: 'static,
|
||||||
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
|
|
||||||
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
|
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
|
||||||
U::Error: fmt::Display + Into<Error>,
|
U::Future: 'static,
|
||||||
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create simple tcp stream service
|
/// Create simple tcp stream service
|
||||||
@@ -82,7 +88,7 @@ where
|
|||||||
Error = DispatchError,
|
Error = DispatchError,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
> {
|
> {
|
||||||
pipeline_factory(|io: TcpStream| {
|
fn_service(|io: TcpStream| {
|
||||||
let peer_addr = io.peer_addr().ok();
|
let peer_addr = io.peer_addr().ok();
|
||||||
ready(Ok((io, peer_addr)))
|
ready(Ok((io, peer_addr)))
|
||||||
})
|
})
|
||||||
@@ -94,29 +100,39 @@ where
|
|||||||
mod openssl {
|
mod openssl {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use actix_service::ServiceFactoryExt;
|
use actix_tls::accept::{
|
||||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
|
openssl::{
|
||||||
use actix_tls::accept::TlsError;
|
reexports::{Error as SslError, SslAcceptor},
|
||||||
|
Acceptor, TlsStream,
|
||||||
|
},
|
||||||
|
TlsError,
|
||||||
|
};
|
||||||
|
|
||||||
impl<S, B, X, U> H1Service<SslStream<TcpStream>, S, B, X, U>
|
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error>,
|
S::Future: 'static,
|
||||||
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
|
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Error: Into<Error>,
|
X::Future: 'static,
|
||||||
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
|
|
||||||
U: ServiceFactory<
|
U: ServiceFactory<
|
||||||
(Request, Framed<SslStream<TcpStream>, Codec>),
|
(Request, Framed<TlsStream<TcpStream>, Codec>),
|
||||||
Config = (),
|
Config = (),
|
||||||
Response = (),
|
Response = (),
|
||||||
>,
|
>,
|
||||||
U::Error: fmt::Display + Into<Error>,
|
U::Future: 'static,
|
||||||
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create openssl based service
|
/// Create OpenSSL based service.
|
||||||
pub fn openssl(
|
pub fn openssl(
|
||||||
self,
|
self,
|
||||||
acceptor: SslAcceptor,
|
acceptor: SslAcceptor,
|
||||||
@@ -127,47 +143,58 @@ mod openssl {
|
|||||||
Error = TlsError<SslError, DispatchError>,
|
Error = TlsError<SslError, DispatchError>,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
> {
|
> {
|
||||||
pipeline_factory(
|
Acceptor::new(acceptor)
|
||||||
Acceptor::new(acceptor)
|
.map_init_err(|_| {
|
||||||
.map_err(TlsError::Tls)
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
.map_init_err(|_| panic!()),
|
})
|
||||||
)
|
.map_err(TlsError::into_service_error)
|
||||||
.and_then(|io: SslStream<TcpStream>| {
|
.map(|io: TlsStream<TcpStream>| {
|
||||||
let peer_addr = io.get_ref().peer_addr().ok();
|
let peer_addr = io.get_ref().peer_addr().ok();
|
||||||
ready(Ok((io, peer_addr)))
|
(io, peer_addr)
|
||||||
})
|
})
|
||||||
.and_then(self.map_err(TlsError::Service))
|
.and_then(self.map_err(TlsError::Service))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
mod rustls {
|
mod rustls {
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use actix_service::ServiceFactoryExt as _;
|
||||||
|
use actix_tls::accept::{
|
||||||
|
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
|
||||||
|
TlsError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
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>
|
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error>,
|
S::Future: 'static,
|
||||||
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
|
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Error: Into<Error>,
|
X::Future: 'static,
|
||||||
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
|
|
||||||
U: ServiceFactory<
|
U: ServiceFactory<
|
||||||
(Request, Framed<TlsStream<TcpStream>, Codec>),
|
(Request, Framed<TlsStream<TcpStream>, Codec>),
|
||||||
Config = (),
|
Config = (),
|
||||||
Response = (),
|
Response = (),
|
||||||
>,
|
>,
|
||||||
U::Error: fmt::Display + Into<Error>,
|
U::Future: 'static,
|
||||||
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create rustls based service
|
/// Create Rustls based service.
|
||||||
pub fn rustls(
|
pub fn rustls(
|
||||||
self,
|
self,
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
@@ -178,16 +205,16 @@ mod rustls {
|
|||||||
Error = TlsError<io::Error, DispatchError>,
|
Error = TlsError<io::Error, DispatchError>,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
> {
|
> {
|
||||||
pipeline_factory(
|
Acceptor::new(config)
|
||||||
Acceptor::new(config)
|
.map_init_err(|_| {
|
||||||
.map_err(TlsError::Tls)
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
.map_init_err(|_| panic!()),
|
})
|
||||||
)
|
.map_err(TlsError::into_service_error)
|
||||||
.and_then(|io: TlsStream<TcpStream>| {
|
.map(|io: TlsStream<TcpStream>| {
|
||||||
let peer_addr = io.get_ref().0.peer_addr().ok();
|
let peer_addr = io.get_ref().0.peer_addr().ok();
|
||||||
ready(Ok((io, peer_addr)))
|
(io, peer_addr)
|
||||||
})
|
})
|
||||||
.and_then(self.map_err(TlsError::Service))
|
.and_then(self.map_err(TlsError::Service))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +222,7 @@ mod rustls {
|
|||||||
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
|
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
@@ -203,7 +230,7 @@ where
|
|||||||
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
|
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
|
||||||
where
|
where
|
||||||
X1: ServiceFactory<Request, Response = Request>,
|
X1: ServiceFactory<Request, Response = Request>,
|
||||||
X1::Error: Into<Error>,
|
X1::Error: Into<Response<BoxBody>>,
|
||||||
X1::InitError: fmt::Debug,
|
X1::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
H1Service {
|
H1Service {
|
||||||
@@ -242,17 +269,24 @@ where
|
|||||||
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
|
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
|
||||||
for H1Service<T, S, B, X, U>
|
for H1Service<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||||
|
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error>,
|
S::Future: 'static,
|
||||||
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
S::InitError: fmt::Debug,
|
S::InitError: fmt::Debug,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
|
|
||||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||||
X::Error: Into<Error>,
|
X::Future: 'static,
|
||||||
|
X::Error: Into<Response<BoxBody>>,
|
||||||
X::InitError: fmt::Debug,
|
X::InitError: fmt::Debug,
|
||||||
|
|
||||||
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
||||||
U::Error: fmt::Display + Into<Error>,
|
U::Future: 'static,
|
||||||
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
type Response = ();
|
type Response = ();
|
||||||
@@ -260,207 +294,77 @@ where
|
|||||||
type Config = ();
|
type Config = ();
|
||||||
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
|
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
|
||||||
type InitError = ();
|
type InitError = ();
|
||||||
type Future = H1ServiceResponse<T, S, B, X, U>;
|
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||||
|
|
||||||
fn new_service(&self, _: ()) -> Self::Future {
|
fn new_service(&self, _: ()) -> Self::Future {
|
||||||
H1ServiceResponse {
|
let service = self.srv.new_service(());
|
||||||
fut: self.srv.new_service(()),
|
let expect = self.expect.new_service(());
|
||||||
fut_ex: Some(self.expect.new_service(())),
|
let upgrade = self.upgrade.as_ref().map(|s| s.new_service(()));
|
||||||
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())),
|
let on_connect_ext = self.on_connect_ext.clone();
|
||||||
expect: None,
|
let cfg = self.cfg.clone();
|
||||||
upgrade: None,
|
|
||||||
on_connect_ext: self.on_connect_ext.clone(),
|
|
||||||
cfg: Some(self.cfg.clone()),
|
|
||||||
_phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
Box::pin(async move {
|
||||||
#[pin_project::pin_project]
|
let expect = expect
|
||||||
pub struct H1ServiceResponse<T, S, B, X, U>
|
.await
|
||||||
where
|
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
|
||||||
S: ServiceFactory<Request>,
|
|
||||||
S::Error: Into<Error>,
|
|
||||||
S::InitError: fmt::Debug,
|
|
||||||
X: ServiceFactory<Request, Response = Request>,
|
|
||||||
X::Error: Into<Error>,
|
|
||||||
X::InitError: fmt::Debug,
|
|
||||||
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
|
|
||||||
U::Error: fmt::Display,
|
|
||||||
U::InitError: fmt::Debug,
|
|
||||||
{
|
|
||||||
#[pin]
|
|
||||||
fut: S::Future,
|
|
||||||
#[pin]
|
|
||||||
fut_ex: Option<X::Future>,
|
|
||||||
#[pin]
|
|
||||||
fut_upg: Option<U::Future>,
|
|
||||||
expect: Option<X::Service>,
|
|
||||||
upgrade: Option<U::Service>,
|
|
||||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
|
||||||
cfg: Option<ServiceConfig>,
|
|
||||||
_phantom: PhantomData<B>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, S, B, X, U> Future for H1ServiceResponse<T, S, B, X, U>
|
let upgrade = match upgrade {
|
||||||
where
|
Some(upgrade) => {
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
let upgrade = upgrade.await.map_err(|e| {
|
||||||
S: ServiceFactory<Request>,
|
log::error!("Init http upgrade service error: {:?}", e)
|
||||||
S::Error: Into<Error>,
|
})?;
|
||||||
S::Response: Into<Response<B>>,
|
Some(upgrade)
|
||||||
S::InitError: fmt::Debug,
|
}
|
||||||
B: MessageBody,
|
None => None,
|
||||||
X: ServiceFactory<Request, Response = Request>,
|
};
|
||||||
X::Error: Into<Error>,
|
|
||||||
X::InitError: fmt::Debug,
|
|
||||||
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
|
|
||||||
U::Error: fmt::Display,
|
|
||||||
U::InitError: fmt::Debug,
|
|
||||||
{
|
|
||||||
type Output = Result<H1ServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
|
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
let service = service
|
||||||
let mut this = self.as_mut().project();
|
.await
|
||||||
|
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
|
||||||
|
|
||||||
if let Some(fut) = this.fut_ex.as_pin_mut() {
|
Ok(H1ServiceHandler::new(
|
||||||
let expect = ready!(fut
|
cfg,
|
||||||
.poll(cx)
|
|
||||||
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
|
|
||||||
this = self.as_mut().project();
|
|
||||||
*this.expect = Some(expect);
|
|
||||||
this.fut_ex.set(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(fut) = this.fut_upg.as_pin_mut() {
|
|
||||||
let upgrade = ready!(fut
|
|
||||||
.poll(cx)
|
|
||||||
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
|
|
||||||
this = self.as_mut().project();
|
|
||||||
*this.upgrade = Some(upgrade);
|
|
||||||
this.fut_upg.set(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = ready!(this
|
|
||||||
.fut
|
|
||||||
.poll(cx)
|
|
||||||
.map_err(|e| log::error!("Init http service error: {:?}", e)));
|
|
||||||
|
|
||||||
Poll::Ready(result.map(|service| {
|
|
||||||
let this = self.as_mut().project();
|
|
||||||
|
|
||||||
H1ServiceHandler::new(
|
|
||||||
this.cfg.take().unwrap(),
|
|
||||||
service,
|
service,
|
||||||
this.expect.take().unwrap(),
|
expect,
|
||||||
this.upgrade.take(),
|
upgrade,
|
||||||
this.on_connect_ext.clone(),
|
on_connect_ext,
|
||||||
)
|
))
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Service` implementation for HTTP/1 transport
|
/// `Service` implementation for HTTP/1 transport
|
||||||
pub struct H1ServiceHandler<T, S, B, X, U>
|
pub type H1ServiceHandler<T, S, B, X, U> = HttpServiceHandler<T, S, B, X, U>;
|
||||||
where
|
|
||||||
S: Service<Request>,
|
|
||||||
X: Service<Request>,
|
|
||||||
U: Service<(Request, Framed<T, Codec>)>,
|
|
||||||
{
|
|
||||||
flow: Rc<RefCell<HttpFlow<S, X, U>>>,
|
|
||||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
|
||||||
cfg: ServiceConfig,
|
|
||||||
_phantom: PhantomData<B>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, S, B, X, U> H1ServiceHandler<T, S, B, X, U>
|
|
||||||
where
|
|
||||||
S: Service<Request>,
|
|
||||||
S::Error: Into<Error>,
|
|
||||||
S::Response: Into<Response<B>>,
|
|
||||||
B: MessageBody,
|
|
||||||
X: Service<Request, Response = Request>,
|
|
||||||
X::Error: Into<Error>,
|
|
||||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
|
||||||
U::Error: fmt::Display,
|
|
||||||
{
|
|
||||||
fn new(
|
|
||||||
cfg: ServiceConfig,
|
|
||||||
service: S,
|
|
||||||
expect: X,
|
|
||||||
upgrade: Option<U>,
|
|
||||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
|
||||||
) -> H1ServiceHandler<T, S, B, X, U> {
|
|
||||||
H1ServiceHandler {
|
|
||||||
flow: HttpFlow::new(service, expect, upgrade),
|
|
||||||
cfg,
|
|
||||||
on_connect_ext,
|
|
||||||
_phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
|
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
|
||||||
for H1ServiceHandler<T, S, B, X, U>
|
for HttpServiceHandler<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Error>,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::Response: Into<Response<B>>,
|
S::Response: Into<Response<B>>,
|
||||||
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
|
|
||||||
X: Service<Request, Response = Request>,
|
X: Service<Request, Response = Request>,
|
||||||
X::Error: Into<Error>,
|
X::Error: Into<Response<BoxBody>>,
|
||||||
|
|
||||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||||
U::Error: fmt::Display + Into<Error>,
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
{
|
{
|
||||||
type Response = ();
|
type Response = ();
|
||||||
type Error = DispatchError;
|
type Error = DispatchError;
|
||||||
type Future = Dispatcher<T, S, B, X, U>;
|
type Future = Dispatcher<T, S, B, X, U>;
|
||||||
|
|
||||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
let mut flow = self.flow.borrow_mut();
|
self._poll_ready(cx).map_err(|e| {
|
||||||
let ready = flow
|
log::error!("HTTP/1 service readiness error: {:?}", e);
|
||||||
.expect
|
DispatchError::Service(e)
|
||||||
.poll_ready(cx)
|
})
|
||||||
.map_err(|e| {
|
|
||||||
let e = e.into();
|
|
||||||
log::error!("Http service readiness error: {:?}", e);
|
|
||||||
DispatchError::Service(e)
|
|
||||||
})?
|
|
||||||
.is_ready();
|
|
||||||
|
|
||||||
let ready = flow
|
|
||||||
.service
|
|
||||||
.poll_ready(cx)
|
|
||||||
.map_err(|e| {
|
|
||||||
let e = e.into();
|
|
||||||
log::error!("Http service readiness error: {:?}", e);
|
|
||||||
DispatchError::Service(e)
|
|
||||||
})?
|
|
||||||
.is_ready()
|
|
||||||
&& ready;
|
|
||||||
|
|
||||||
let ready = if let Some(ref mut upg) = flow.upgrade {
|
|
||||||
upg.poll_ready(cx)
|
|
||||||
.map_err(|e| {
|
|
||||||
let e = e.into();
|
|
||||||
log::error!("Http service readiness error: {:?}", e);
|
|
||||||
DispatchError::Service(e)
|
|
||||||
})?
|
|
||||||
.is_ready()
|
|
||||||
&& ready
|
|
||||||
} else {
|
|
||||||
ready
|
|
||||||
};
|
|
||||||
|
|
||||||
if ready {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
} else {
|
|
||||||
Poll::Pending
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call(&mut self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||||
let on_connect_data =
|
let on_connect_data =
|
||||||
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use std::task::{Context, Poll};
|
|
||||||
|
|
||||||
use actix_codec::Framed;
|
use actix_codec::Framed;
|
||||||
use actix_service::{Service, ServiceFactory};
|
use actix_service::{Service, ServiceFactory};
|
||||||
use futures_util::future::{ready, Ready};
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::h1::Codec;
|
use crate::h1::Codec;
|
||||||
@@ -16,7 +14,7 @@ impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
|
|||||||
type Config = ();
|
type Config = ();
|
||||||
type Service = UpgradeHandler;
|
type Service = UpgradeHandler;
|
||||||
type InitError = Error;
|
type InitError = Error;
|
||||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||||
|
|
||||||
fn new_service(&self, _: ()) -> Self::Future {
|
fn new_service(&self, _: ()) -> Self::Future {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
@@ -26,13 +24,11 @@ impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
|
|||||||
impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler {
|
impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler {
|
||||||
type Response = ();
|
type Response = ();
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
actix_service::always_ready!();
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call(&mut self, _: (Request, Framed<T, Codec>)) -> Self::Future {
|
fn call(&self, _: (Request, Framed<T, Codec>)) -> Self::Future {
|
||||||
ready(Ok(()))
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
use std::future::Future;
|
use std::{
|
||||||
use std::pin::Pin;
|
future::Future,
|
||||||
use std::task::{Context, Poll};
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
use crate::body::{BodySize, MessageBody, ResponseBody};
|
use crate::{
|
||||||
use crate::error::Error;
|
body::{BodySize, MessageBody},
|
||||||
use crate::h1::{Codec, Message};
|
error::Error,
|
||||||
use crate::response::Response;
|
h1::{Codec, Message},
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
|
||||||
/// Send HTTP/1 response
|
pin_project! {
|
||||||
#[pin_project::pin_project]
|
/// Send HTTP/1 response
|
||||||
pub struct SendResponse<T, B> {
|
pub struct SendResponse<T, B> {
|
||||||
res: Option<Message<(Response<()>, BodySize)>>,
|
res: Option<Message<(Response<()>, BodySize)>>,
|
||||||
#[pin]
|
|
||||||
body: Option<ResponseBody<B>>,
|
#[pin]
|
||||||
#[pin]
|
body: Option<B>,
|
||||||
framed: Option<Framed<T, Codec>>,
|
|
||||||
|
#[pin]
|
||||||
|
framed: Option<Framed<T, Codec>>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, B> SendResponse<T, B>
|
impl<T, B> SendResponse<T, B>
|
||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
|
B::Error: Into<Error>,
|
||||||
{
|
{
|
||||||
pub fn new(framed: Framed<T, Codec>, response: Response<B>) -> Self {
|
pub fn new(framed: Framed<T, Codec>, response: Response<B>) -> Self {
|
||||||
let (res, body) = response.into_parts();
|
let (res, body) = response.into_parts();
|
||||||
@@ -38,6 +47,7 @@ impl<T, B> Future for SendResponse<T, B>
|
|||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
B: MessageBody + Unpin,
|
B: MessageBody + Unpin,
|
||||||
|
B::Error: Into<Error>,
|
||||||
{
|
{
|
||||||
type Output = Result<Framed<T, Codec>, Error>;
|
type Output = Result<Framed<T, Codec>, Error>;
|
||||||
|
|
||||||
@@ -60,7 +70,17 @@ where
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.is_write_buf_full()
|
.is_write_buf_full()
|
||||||
{
|
{
|
||||||
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? {
|
let next =
|
||||||
|
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) {
|
||||||
|
Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)),
|
||||||
|
Poll::Ready(Some(Err(err))) => {
|
||||||
|
return Poll::Ready(Err(err.into()))
|
||||||
|
}
|
||||||
|
Poll::Ready(None) => Poll::Ready(None),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
};
|
||||||
|
|
||||||
|
match next {
|
||||||
Poll::Ready(item) => {
|
Poll::Ready(item) => {
|
||||||
// body is done when item is None
|
// body is done when item is None
|
||||||
body_done = item.is_none();
|
body_done = item.is_none();
|
||||||
@@ -68,7 +88,9 @@ where
|
|||||||
let _ = this.body.take();
|
let _ = this.body.take();
|
||||||
}
|
}
|
||||||
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
||||||
framed.write(Message::Chunk(item))?;
|
framed.write(Message::Chunk(item)).map_err(|err| {
|
||||||
|
Error::new_send_response().with_cause(err)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
Poll::Pending => body_ready = false,
|
Poll::Pending => body_ready = false,
|
||||||
}
|
}
|
||||||
@@ -79,7 +101,10 @@ where
|
|||||||
|
|
||||||
// flush write buffer
|
// flush write buffer
|
||||||
if !framed.is_write_buf_empty() {
|
if !framed.is_write_buf_empty() {
|
||||||
match framed.flush(cx)? {
|
match framed
|
||||||
|
.flush(cx)
|
||||||
|
.map_err(|err| Error::new_send_response().with_cause(err))?
|
||||||
|
{
|
||||||
Poll::Ready(_) => {
|
Poll::Ready(_) => {
|
||||||
if body_ready {
|
if body_ready {
|
||||||
continue;
|
continue;
|
||||||
@@ -93,7 +118,9 @@ where
|
|||||||
|
|
||||||
// send response
|
// send response
|
||||||
if let Some(res) = this.res.take() {
|
if let Some(res) = this.res.take() {
|
||||||
framed.write(res)?;
|
framed
|
||||||
|
.write(res)
|
||||||
|
.map_err(|err| Error::new_send_response().with_cause(err))?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +1,118 @@
|
|||||||
use std::cell::RefCell;
|
use std::{
|
||||||
use std::future::Future;
|
cmp,
|
||||||
use std::marker::PhantomData;
|
error::Error as StdError,
|
||||||
use std::net;
|
future::Future,
|
||||||
use std::pin::Pin;
|
marker::PhantomData,
|
||||||
use std::rc::Rc;
|
net,
|
||||||
use std::task::{Context, Poll};
|
pin::Pin,
|
||||||
use std::{cmp, convert::TryFrom};
|
rc::Rc,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite};
|
use actix_codec::{AsyncRead, AsyncWrite};
|
||||||
use actix_rt::time::{Instant, Sleep};
|
use actix_rt::time::{sleep, Sleep};
|
||||||
use actix_service::Service;
|
use actix_service::Service;
|
||||||
|
use actix_utils::future::poll_fn;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use h2::server::{Connection, SendResponse};
|
use h2::{
|
||||||
use h2::SendStream;
|
server::{Connection, SendResponse},
|
||||||
|
Ping, PingPong,
|
||||||
|
};
|
||||||
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
|
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
|
||||||
use log::{error, trace};
|
use log::{error, trace};
|
||||||
|
use pin_project_lite::pin_project;
|
||||||
|
|
||||||
use crate::body::{BodySize, MessageBody, ResponseBody};
|
use crate::{
|
||||||
use crate::config::ServiceConfig;
|
body::{BodySize, BoxBody, MessageBody},
|
||||||
use crate::error::{DispatchError, Error};
|
config::ServiceConfig,
|
||||||
use crate::message::ResponseHead;
|
service::HttpFlow,
|
||||||
use crate::payload::Payload;
|
OnConnectData, Payload, Request, Response, ResponseHead,
|
||||||
use crate::request::Request;
|
};
|
||||||
use crate::response::Response;
|
|
||||||
use crate::service::HttpFlow;
|
|
||||||
use crate::OnConnectData;
|
|
||||||
|
|
||||||
const CHUNK_SIZE: usize = 16_384;
|
const CHUNK_SIZE: usize = 16_384;
|
||||||
|
|
||||||
/// Dispatcher for HTTP/2 protocol.
|
pin_project! {
|
||||||
#[pin_project::pin_project]
|
/// Dispatcher for HTTP/2 protocol.
|
||||||
pub struct Dispatcher<T, S, B, X, U>
|
pub struct Dispatcher<T, S, B, X, U> {
|
||||||
where
|
flow: Rc<HttpFlow<S, X, U>>,
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
connection: Connection<T, Bytes>,
|
||||||
S: Service<Request>,
|
on_connect_data: OnConnectData,
|
||||||
B: MessageBody,
|
config: ServiceConfig,
|
||||||
{
|
peer_addr: Option<net::SocketAddr>,
|
||||||
flow: Rc<RefCell<HttpFlow<S, X, U>>>,
|
ping_pong: Option<H2PingPong>,
|
||||||
connection: Connection<T, Bytes>,
|
_phantom: PhantomData<B>
|
||||||
on_connect_data: OnConnectData,
|
}
|
||||||
config: ServiceConfig,
|
|
||||||
peer_addr: Option<net::SocketAddr>,
|
|
||||||
ka_expire: Instant,
|
|
||||||
ka_timer: Option<Sleep>,
|
|
||||||
_phantom: PhantomData<B>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
|
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
S: Service<Request>,
|
|
||||||
S::Error: Into<Error>,
|
|
||||||
S::Response: Into<Response<B>>,
|
|
||||||
B: MessageBody,
|
|
||||||
{
|
{
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
services: Rc<RefCell<HttpFlow<S, X, U>>>,
|
flow: Rc<HttpFlow<S, X, U>>,
|
||||||
connection: Connection<T, Bytes>,
|
mut conn: Connection<T, Bytes>,
|
||||||
on_connect_data: OnConnectData,
|
on_connect_data: OnConnectData,
|
||||||
config: ServiceConfig,
|
config: ServiceConfig,
|
||||||
timeout: Option<Sleep>,
|
|
||||||
peer_addr: Option<net::SocketAddr>,
|
peer_addr: Option<net::SocketAddr>,
|
||||||
|
timer: Option<Pin<Box<Sleep>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// let keepalive = config.keep_alive_enabled();
|
let ping_pong = config.keep_alive().map(|dur| H2PingPong {
|
||||||
// let flags = if keepalive {
|
timer: timer
|
||||||
// Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED
|
.map(|mut timer| {
|
||||||
// } else {
|
// reset timer if it's received from new function.
|
||||||
// Flags::empty()
|
timer.as_mut().reset(config.now() + dur);
|
||||||
// };
|
timer
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| Box::pin(sleep(dur))),
|
||||||
|
on_flight: false,
|
||||||
|
ping_pong: conn.ping_pong().unwrap(),
|
||||||
|
});
|
||||||
|
|
||||||
// keep-alive timer
|
Self {
|
||||||
let (ka_expire, ka_timer) = if let Some(delay) = timeout {
|
flow,
|
||||||
(delay.deadline(), Some(delay))
|
|
||||||
} else if let Some(delay) = config.keep_alive_timer() {
|
|
||||||
(delay.deadline(), Some(delay))
|
|
||||||
} else {
|
|
||||||
(config.now(), None)
|
|
||||||
};
|
|
||||||
|
|
||||||
Dispatcher {
|
|
||||||
flow: services,
|
|
||||||
config,
|
config,
|
||||||
peer_addr,
|
peer_addr,
|
||||||
connection,
|
connection: conn,
|
||||||
on_connect_data,
|
on_connect_data,
|
||||||
ka_expire,
|
ping_pong,
|
||||||
ka_timer,
|
|
||||||
_phantom: PhantomData,
|
_phantom: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct H2PingPong {
|
||||||
|
timer: Pin<Box<Sleep>>,
|
||||||
|
on_flight: bool,
|
||||||
|
ping_pong: PingPong,
|
||||||
|
}
|
||||||
|
|
||||||
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
|
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>>,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>>,
|
||||||
B: MessageBody + 'static,
|
|
||||||
|
B: MessageBody,
|
||||||
{
|
{
|
||||||
type Output = Result<(), DispatchError>;
|
type Output = Result<(), crate::error::DispatchError>;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match ready!(Pin::new(&mut this.connection).poll_accept(cx)) {
|
match Pin::new(&mut this.connection).poll_accept(cx)? {
|
||||||
None => return Poll::Ready(Ok(())),
|
Poll::Ready(Some((req, tx))) => {
|
||||||
|
|
||||||
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() {
|
|
||||||
this.ka_expire = expire;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (parts, body) = req.into_parts();
|
let (parts, body) = req.into_parts();
|
||||||
let pl = crate::h2::Payload::new(body);
|
let pl = crate::h2::Payload::new(body);
|
||||||
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
|
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
|
||||||
let mut req = Request::with_payload(pl);
|
let mut req = Request::with_payload(pl);
|
||||||
|
|
||||||
let head = &mut req.head_mut();
|
let head = req.head_mut();
|
||||||
head.uri = parts.uri;
|
head.uri = parts.uri;
|
||||||
head.method = parts.method;
|
head.method = parts.method;
|
||||||
head.version = parts.version;
|
head.version = parts.version;
|
||||||
@@ -136,236 +122,214 @@ where
|
|||||||
// merge on_connect_ext data into request extensions
|
// merge on_connect_ext data into request extensions
|
||||||
this.on_connect_data.merge_into(&mut req);
|
this.on_connect_data.merge_into(&mut req);
|
||||||
|
|
||||||
let svc = ServiceResponse::<S::Future, S::Response, S::Error, B> {
|
let fut = this.flow.service.call(req);
|
||||||
state: ServiceResponseState::ServiceCall(
|
let config = this.config.clone();
|
||||||
this.flow.borrow_mut().service.call(req),
|
|
||||||
Some(res),
|
|
||||||
),
|
|
||||||
config: this.config.clone(),
|
|
||||||
buffer: None,
|
|
||||||
_phantom: PhantomData,
|
|
||||||
};
|
|
||||||
|
|
||||||
actix_rt::spawn(svc);
|
// multiplex request handling with spawn task
|
||||||
|
actix_rt::spawn(async move {
|
||||||
|
// resolve service call and send response.
|
||||||
|
let res = match fut.await {
|
||||||
|
Ok(res) => handle_response(res.into(), tx, config).await,
|
||||||
|
Err(err) => {
|
||||||
|
let res: Response<BoxBody> = err.into();
|
||||||
|
handle_response(res, tx, config).await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// log error.
|
||||||
|
if let Err(err) = res {
|
||||||
|
match err {
|
||||||
|
DispatchError::SendResponse(err) => {
|
||||||
|
trace!("Error sending HTTP/2 response: {:?}", err)
|
||||||
|
}
|
||||||
|
DispatchError::SendData(err) => warn!("{:?}", err),
|
||||||
|
DispatchError::ResponseBody(err) => {
|
||||||
|
error!("Response payload stream error: {:?}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Poll::Ready(None) => return Poll::Ready(Ok(())),
|
||||||
|
Poll::Pending => match this.ping_pong.as_mut() {
|
||||||
|
Some(ping_pong) => loop {
|
||||||
|
if ping_pong.on_flight {
|
||||||
|
// When have on flight ping pong. poll pong and and keep alive timer.
|
||||||
|
// on success pong received update keep alive timer to determine the next timing of
|
||||||
|
// ping pong.
|
||||||
|
match ping_pong.ping_pong.poll_pong(cx)? {
|
||||||
|
Poll::Ready(_) => {
|
||||||
|
ping_pong.on_flight = false;
|
||||||
|
|
||||||
|
let dead_line =
|
||||||
|
this.config.keep_alive_expire().unwrap();
|
||||||
|
ping_pong.timer.as_mut().reset(dead_line);
|
||||||
|
}
|
||||||
|
Poll::Pending => {
|
||||||
|
return ping_pong
|
||||||
|
.timer
|
||||||
|
.as_mut()
|
||||||
|
.poll(cx)
|
||||||
|
.map(|_| Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// When there is no on flight ping pong. keep alive timer is used to wait for next
|
||||||
|
// timing of ping pong. Therefore at this point it serves as an interval instead.
|
||||||
|
ready!(ping_pong.timer.as_mut().poll(cx));
|
||||||
|
|
||||||
|
ping_pong.ping_pong.send_ping(Ping::opaque())?;
|
||||||
|
|
||||||
|
let dead_line = this.config.keep_alive_expire().unwrap();
|
||||||
|
ping_pong.timer.as_mut().reset(dead_line);
|
||||||
|
|
||||||
|
ping_pong.on_flight = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => return Poll::Pending,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pin_project::pin_project]
|
enum DispatchError {
|
||||||
struct ServiceResponse<F, I, E, B> {
|
SendResponse(h2::Error),
|
||||||
#[pin]
|
SendData(h2::Error),
|
||||||
state: ServiceResponseState<F, B>,
|
ResponseBody(Box<dyn StdError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_response<B>(
|
||||||
|
res: Response<B>,
|
||||||
|
mut tx: SendResponse<Bytes>,
|
||||||
config: ServiceConfig,
|
config: ServiceConfig,
|
||||||
buffer: Option<Bytes>,
|
) -> Result<(), DispatchError>
|
||||||
_phantom: PhantomData<(I, E)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pin_project::pin_project(project = ServiceResponseStateProj)]
|
|
||||||
enum ServiceResponseState<F, B> {
|
|
||||||
ServiceCall(#[pin] F, Option<SendResponse<Bytes>>),
|
|
||||||
SendPayload(SendStream<Bytes>, #[pin] ResponseBody<B>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F, I, E, B> ServiceResponse<F, I, E, B>
|
|
||||||
where
|
where
|
||||||
F: Future<Output = Result<I, E>>,
|
|
||||||
E: Into<Error>,
|
|
||||||
I: Into<Response<B>>,
|
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
{
|
{
|
||||||
fn prepare_response(
|
let (res, body) = res.replace_body(());
|
||||||
&self,
|
|
||||||
head: &ResponseHead,
|
|
||||||
size: &mut BodySize,
|
|
||||||
) -> http::Response<()> {
|
|
||||||
let mut has_date = false;
|
|
||||||
let mut skip_len = size != &BodySize::Stream;
|
|
||||||
|
|
||||||
let mut res = http::Response::new(());
|
// prepare response.
|
||||||
*res.status_mut() = head.status;
|
let mut size = body.size();
|
||||||
*res.version_mut() = http::Version::HTTP_2;
|
let res = prepare_response(config, res.head(), &mut size);
|
||||||
|
let eof = size.is_eof();
|
||||||
|
|
||||||
// Content length
|
// send response head and return on eof.
|
||||||
match head.status {
|
let mut stream = tx
|
||||||
http::StatusCode::NO_CONTENT
|
.send_response(res, eof)
|
||||||
| http::StatusCode::CONTINUE
|
.map_err(DispatchError::SendResponse)?;
|
||||||
| http::StatusCode::PROCESSING => *size = BodySize::None,
|
|
||||||
http::StatusCode::SWITCHING_PROTOCOLS => {
|
if eof {
|
||||||
skip_len = true;
|
return Ok(());
|
||||||
*size = BodySize::Stream;
|
}
|
||||||
|
|
||||||
|
// poll response body and send chunks to client.
|
||||||
|
actix_rt::pin!(body);
|
||||||
|
|
||||||
|
while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
||||||
|
let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?;
|
||||||
|
|
||||||
|
'send: loop {
|
||||||
|
// reserve enough space and wait for stream ready.
|
||||||
|
stream.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
|
||||||
|
|
||||||
|
match poll_fn(|cx| stream.poll_capacity(cx)).await {
|
||||||
|
// No capacity left. drop body and return.
|
||||||
|
None => return Ok(()),
|
||||||
|
Some(res) => {
|
||||||
|
// Split chuck to writeable size and send to client.
|
||||||
|
let cap = res.map_err(DispatchError::SendData)?;
|
||||||
|
|
||||||
|
let len = chunk.len();
|
||||||
|
let bytes = chunk.split_to(cmp::min(cap, len));
|
||||||
|
|
||||||
|
stream
|
||||||
|
.send_data(bytes, false)
|
||||||
|
.map_err(DispatchError::SendData)?;
|
||||||
|
|
||||||
|
// Current chuck completely sent. break send loop and poll next one.
|
||||||
|
if chunk.is_empty() {
|
||||||
|
break 'send;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// response body streaming finished. send end of stream and return.
|
||||||
|
stream
|
||||||
|
.send_data(Bytes::new(), true)
|
||||||
|
.map_err(DispatchError::SendData)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_response(
|
||||||
|
config: ServiceConfig,
|
||||||
|
head: &ResponseHead,
|
||||||
|
size: &mut BodySize,
|
||||||
|
) -> http::Response<()> {
|
||||||
|
let mut has_date = false;
|
||||||
|
let mut skip_len = size != &BodySize::Stream;
|
||||||
|
|
||||||
|
let mut res = http::Response::new(());
|
||||||
|
*res.status_mut() = head.status;
|
||||||
|
*res.version_mut() = http::Version::HTTP_2;
|
||||||
|
|
||||||
|
// Content length
|
||||||
|
match head.status {
|
||||||
|
http::StatusCode::NO_CONTENT
|
||||||
|
| http::StatusCode::CONTINUE
|
||||||
|
| http::StatusCode::PROCESSING => *size = BodySize::None,
|
||||||
|
http::StatusCode::SWITCHING_PROTOCOLS => {
|
||||||
|
skip_len = true;
|
||||||
|
*size = BodySize::Stream;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = match size {
|
||||||
|
BodySize::None | BodySize::Stream => None,
|
||||||
|
|
||||||
|
BodySize::Sized(0) => res
|
||||||
|
.headers_mut()
|
||||||
|
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
|
||||||
|
|
||||||
|
BodySize::Sized(len) => {
|
||||||
|
let mut buf = itoa::Buffer::new();
|
||||||
|
|
||||||
|
res.headers_mut().insert(
|
||||||
|
CONTENT_LENGTH,
|
||||||
|
HeaderValue::from_str(buf.format(*len)).unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// copy headers
|
||||||
|
for (key, value) in head.headers.iter() {
|
||||||
|
match *key {
|
||||||
|
// TODO: consider skipping other headers according to:
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2
|
||||||
|
// omit HTTP/1.x only headers
|
||||||
|
CONNECTION | TRANSFER_ENCODING => continue,
|
||||||
|
CONTENT_LENGTH if skip_len => continue,
|
||||||
|
DATE => has_date = true,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = match size {
|
res.headers_mut().append(key, value.clone());
|
||||||
BodySize::None | BodySize::Stream => None,
|
|
||||||
BodySize::Empty => res
|
|
||||||
.headers_mut()
|
|
||||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
|
|
||||||
BodySize::Sized(len) => res.headers_mut().insert(
|
|
||||||
CONTENT_LENGTH,
|
|
||||||
HeaderValue::try_from(format!("{}", len)).unwrap(),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// copy headers
|
|
||||||
for (key, value) in head.headers.iter() {
|
|
||||||
match *key {
|
|
||||||
// omit HTTP/1 only headers
|
|
||||||
CONNECTION | TRANSFER_ENCODING => continue,
|
|
||||||
CONTENT_LENGTH if skip_len => continue,
|
|
||||||
DATE => has_date = true,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.headers_mut().append(key, value.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// set date header
|
|
||||||
if !has_date {
|
|
||||||
let mut bytes = BytesMut::with_capacity(29);
|
|
||||||
self.config.set_date_header(&mut bytes);
|
|
||||||
res.headers_mut().insert(
|
|
||||||
DATE,
|
|
||||||
// SAFETY: serialized date-times are known ASCII strings
|
|
||||||
unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<F, I, E, B> Future for ServiceResponse<F, I, E, B>
|
// set date header
|
||||||
where
|
if !has_date {
|
||||||
F: Future<Output = Result<I, E>>,
|
let mut bytes = BytesMut::with_capacity(29);
|
||||||
E: Into<Error>,
|
config.set_date_header(&mut bytes);
|
||||||
I: Into<Response<B>>,
|
res.headers_mut().insert(
|
||||||
B: MessageBody,
|
DATE,
|
||||||
{
|
// SAFETY: serialized date-times are known ASCII strings
|
||||||
type Output = ();
|
unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) },
|
||||||
|
);
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
||||||
let mut this = self.as_mut().project();
|
|
||||||
|
|
||||||
match this.state.project() {
|
|
||||||
ServiceResponseStateProj::ServiceCall(call, send) => {
|
|
||||||
match ready!(call.poll(cx)) {
|
|
||||||
Ok(res) => {
|
|
||||||
let (res, body) = res.into().replace_body(());
|
|
||||||
|
|
||||||
let mut send = send.take().unwrap();
|
|
||||||
let mut size = body.size();
|
|
||||||
let h2_res =
|
|
||||||
self.as_mut().prepare_response(res.head(), &mut size);
|
|
||||||
this = self.as_mut().project();
|
|
||||||
|
|
||||||
let stream = match send.send_response(h2_res, size.is_eof()) {
|
|
||||||
Err(e) => {
|
|
||||||
trace!("Error sending HTTP/2 response: {:?}", e);
|
|
||||||
return Poll::Ready(());
|
|
||||||
}
|
|
||||||
Ok(stream) => stream,
|
|
||||||
};
|
|
||||||
|
|
||||||
if size.is_eof() {
|
|
||||||
Poll::Ready(())
|
|
||||||
} else {
|
|
||||||
this.state
|
|
||||||
.set(ServiceResponseState::SendPayload(stream, body));
|
|
||||||
self.poll(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(e) => {
|
|
||||||
let res: Response = e.into().into();
|
|
||||||
let (res, body) = res.replace_body(());
|
|
||||||
|
|
||||||
let mut send = send.take().unwrap();
|
|
||||||
let mut size = body.size();
|
|
||||||
let h2_res =
|
|
||||||
self.as_mut().prepare_response(res.head(), &mut size);
|
|
||||||
this = self.as_mut().project();
|
|
||||||
|
|
||||||
let stream = match send.send_response(h2_res, size.is_eof()) {
|
|
||||||
Err(e) => {
|
|
||||||
trace!("Error sending HTTP/2 response: {:?}", e);
|
|
||||||
return Poll::Ready(());
|
|
||||||
}
|
|
||||||
Ok(stream) => stream,
|
|
||||||
};
|
|
||||||
|
|
||||||
if size.is_eof() {
|
|
||||||
Poll::Ready(())
|
|
||||||
} else {
|
|
||||||
this.state.set(ServiceResponseState::SendPayload(
|
|
||||||
stream,
|
|
||||||
body.into_body(),
|
|
||||||
));
|
|
||||||
self.poll(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => {
|
|
||||||
loop {
|
|
||||||
loop {
|
|
||||||
match this.buffer {
|
|
||||||
Some(ref mut buffer) => {
|
|
||||||
match ready!(stream.poll_capacity(cx)) {
|
|
||||||
None => return Poll::Ready(()),
|
|
||||||
|
|
||||||
Some(Ok(cap)) => {
|
|
||||||
let len = buffer.len();
|
|
||||||
let bytes = buffer.split_to(cmp::min(cap, len));
|
|
||||||
|
|
||||||
if let Err(e) = stream.send_data(bytes, false) {
|
|
||||||
warn!("{:?}", e);
|
|
||||||
return Poll::Ready(());
|
|
||||||
} else if !buffer.is_empty() {
|
|
||||||
let cap = cmp::min(buffer.len(), CHUNK_SIZE);
|
|
||||||
stream.reserve_capacity(cap);
|
|
||||||
} else {
|
|
||||||
this.buffer.take();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Err(e)) => {
|
|
||||||
warn!("{:?}", e);
|
|
||||||
return Poll::Ready(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None => match ready!(body.as_mut().poll_next(cx)) {
|
|
||||||
None => {
|
|
||||||
if let Err(e) = stream.send_data(Bytes::new(), true)
|
|
||||||
{
|
|
||||||
warn!("{:?}", e);
|
|
||||||
}
|
|
||||||
return Poll::Ready(());
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Ok(chunk)) => {
|
|
||||||
stream.reserve_capacity(cmp::min(
|
|
||||||
chunk.len(),
|
|
||||||
CHUNK_SIZE,
|
|
||||||
));
|
|
||||||
*this.buffer = Some(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Err(e)) => {
|
|
||||||
error!("Response payload stream error: {:?}", e);
|
|
||||||
return Poll::Ready(());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
//! HTTP/2 implementation.
|
//! HTTP/2 protocol.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
future::Future,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use actix_codec::{AsyncRead, AsyncWrite};
|
||||||
|
use actix_rt::time::Sleep;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
use h2::RecvStream;
|
use h2::{
|
||||||
|
server::{handshake, Connection, Handshake},
|
||||||
|
RecvStream,
|
||||||
|
};
|
||||||
|
|
||||||
mod dispatcher;
|
mod dispatcher;
|
||||||
mod service;
|
mod service;
|
||||||
|
|
||||||
pub use self::dispatcher::Dispatcher;
|
pub use self::dispatcher::Dispatcher;
|
||||||
pub use self::service::H2Service;
|
pub use self::service::H2Service;
|
||||||
use crate::error::PayloadError;
|
|
||||||
|
use crate::{
|
||||||
|
config::ServiceConfig,
|
||||||
|
error::{DispatchError, PayloadError},
|
||||||
|
};
|
||||||
|
|
||||||
/// HTTP/2 peer stream.
|
/// HTTP/2 peer stream.
|
||||||
pub struct Payload {
|
pub struct Payload {
|
||||||
@@ -50,3 +60,44 @@ impl Stream for Payload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handshake_with_timeout<T>(
|
||||||
|
io: T,
|
||||||
|
config: &ServiceConfig,
|
||||||
|
) -> HandshakeWithTimeout<T>
|
||||||
|
where
|
||||||
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
HandshakeWithTimeout {
|
||||||
|
handshake: handshake(io),
|
||||||
|
timer: config.client_timer().map(Box::pin),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct HandshakeWithTimeout<T: AsyncRead + AsyncWrite + Unpin> {
|
||||||
|
handshake: Handshake<T>,
|
||||||
|
timer: Option<Pin<Box<Sleep>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Future for HandshakeWithTimeout<T>
|
||||||
|
where
|
||||||
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
type Output = Result<(Connection<T, Bytes>, Option<Pin<Box<Sleep>>>), DispatchError>;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
let this = self.get_mut();
|
||||||
|
|
||||||
|
match Pin::new(&mut this.handshake).poll(cx)? {
|
||||||
|
// return the timer on success handshake. It can be re-used for h2 ping-pong.
|
||||||
|
Poll::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))),
|
||||||
|
Poll::Pending => match this.timer.as_mut() {
|
||||||
|
Some(timer) => {
|
||||||
|
ready!(timer.as_mut().poll(cx));
|
||||||
|
Poll::Ready(Err(DispatchError::SlowRequestTimeout))
|
||||||
|
}
|
||||||
|
None => Poll::Pending,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
use std::cell::RefCell;
|
use std::{
|
||||||
use std::future::Future;
|
future::Future,
|
||||||
use std::marker::PhantomData;
|
marker::PhantomData,
|
||||||
use std::pin::Pin;
|
net,
|
||||||
use std::task::{Context, Poll};
|
pin::Pin,
|
||||||
use std::{net, rc::Rc};
|
rc::Rc,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite};
|
use actix_codec::{AsyncRead, AsyncWrite};
|
||||||
use actix_rt::net::TcpStream;
|
use actix_rt::net::TcpStream;
|
||||||
use actix_service::{
|
use actix_service::{
|
||||||
fn_factory, fn_service, pipeline_factory, IntoServiceFactory, Service,
|
fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory,
|
||||||
ServiceFactory,
|
ServiceFactoryExt as _,
|
||||||
};
|
};
|
||||||
use bytes::Bytes;
|
use actix_utils::future::ready;
|
||||||
use futures_core::ready;
|
use futures_core::{future::LocalBoxFuture, ready};
|
||||||
use futures_util::future::ok;
|
|
||||||
use h2::server::{self, Handshake};
|
|
||||||
use log::error;
|
use log::error;
|
||||||
|
|
||||||
use crate::body::MessageBody;
|
use crate::{
|
||||||
use crate::config::ServiceConfig;
|
body::{BoxBody, MessageBody},
|
||||||
use crate::error::{DispatchError, Error};
|
config::ServiceConfig,
|
||||||
use crate::request::Request;
|
error::DispatchError,
|
||||||
use crate::response::Response;
|
service::HttpFlow,
|
||||||
use crate::service::HttpFlow;
|
ConnectCallback, OnConnectData, Request, Response,
|
||||||
use crate::{ConnectCallback, OnConnectData};
|
};
|
||||||
|
|
||||||
use super::dispatcher::Dispatcher;
|
use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout};
|
||||||
|
|
||||||
/// `ServiceFactory` implementation for HTTP/2 transport
|
/// `ServiceFactory` implementation for HTTP/2 transport
|
||||||
pub struct H2Service<T, S, B> {
|
pub struct H2Service<T, S, B> {
|
||||||
@@ -38,9 +38,10 @@ pub struct H2Service<T, S, B> {
|
|||||||
impl<T, S, B> H2Service<T, S, B>
|
impl<T, S, B> H2Service<T, S, B>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
/// Create new `H2Service` instance with config.
|
/// Create new `H2Service` instance with config.
|
||||||
@@ -66,9 +67,11 @@ where
|
|||||||
impl<S, B> H2Service<TcpStream, S, B>
|
impl<S, B> H2Service<TcpStream, S, B>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Future: 'static,
|
||||||
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
/// Create plain TCP based service
|
/// Create plain TCP based service
|
||||||
@@ -81,33 +84,40 @@ where
|
|||||||
Error = DispatchError,
|
Error = DispatchError,
|
||||||
InitError = S::InitError,
|
InitError = S::InitError,
|
||||||
> {
|
> {
|
||||||
pipeline_factory(fn_factory(|| async {
|
fn_factory(|| {
|
||||||
Ok::<_, S::InitError>(fn_service(|io: TcpStream| {
|
ready(Ok::<_, S::InitError>(fn_service(|io: TcpStream| {
|
||||||
let peer_addr = io.peer_addr().ok();
|
let peer_addr = io.peer_addr().ok();
|
||||||
ok::<_, DispatchError>((io, peer_addr))
|
ready(Ok::<_, DispatchError>((io, peer_addr)))
|
||||||
}))
|
})))
|
||||||
}))
|
})
|
||||||
.and_then(self)
|
.and_then(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
mod openssl {
|
mod openssl {
|
||||||
use actix_service::{fn_factory, fn_service, ServiceFactoryExt};
|
use actix_service::ServiceFactoryExt as _;
|
||||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
|
use actix_tls::accept::{
|
||||||
use actix_tls::accept::TlsError;
|
openssl::{
|
||||||
|
reexports::{Error as SslError, SslAcceptor},
|
||||||
|
Acceptor, TlsStream,
|
||||||
|
},
|
||||||
|
TlsError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl<S, B> H2Service<SslStream<TcpStream>, S, B>
|
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Future: 'static,
|
||||||
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
/// Create OpenSSL based service
|
/// Create OpenSSL based service.
|
||||||
pub fn openssl(
|
pub fn openssl(
|
||||||
self,
|
self,
|
||||||
acceptor: SslAcceptor,
|
acceptor: SslAcceptor,
|
||||||
@@ -118,39 +128,43 @@ mod openssl {
|
|||||||
Error = TlsError<SslError, DispatchError>,
|
Error = TlsError<SslError, DispatchError>,
|
||||||
InitError = S::InitError,
|
InitError = S::InitError,
|
||||||
> {
|
> {
|
||||||
pipeline_factory(
|
Acceptor::new(acceptor)
|
||||||
Acceptor::new(acceptor)
|
.map_init_err(|_| {
|
||||||
.map_err(TlsError::Tls)
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
.map_init_err(|_| panic!()),
|
})
|
||||||
)
|
.map_err(TlsError::into_service_error)
|
||||||
.and_then(fn_factory(|| {
|
.map(|io: TlsStream<TcpStream>| {
|
||||||
ok::<_, S::InitError>(fn_service(|io: SslStream<TcpStream>| {
|
|
||||||
let peer_addr = io.get_ref().peer_addr().ok();
|
let peer_addr = io.get_ref().peer_addr().ok();
|
||||||
ok((io, peer_addr))
|
(io, peer_addr)
|
||||||
}))
|
})
|
||||||
}))
|
.and_then(self.map_err(TlsError::Service))
|
||||||
.and_then(self.map_err(TlsError::Service))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
mod rustls {
|
mod rustls {
|
||||||
use super::*;
|
|
||||||
use actix_service::ServiceFactoryExt;
|
|
||||||
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
|
|
||||||
use actix_tls::accept::TlsError;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
|
use actix_service::ServiceFactoryExt as _;
|
||||||
|
use actix_tls::accept::{
|
||||||
|
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
|
||||||
|
TlsError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
|
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
|
||||||
where
|
where
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Future: 'static,
|
||||||
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
/// Create Rustls based service
|
/// Create Rustls based service.
|
||||||
pub fn rustls(
|
pub fn rustls(
|
||||||
self,
|
self,
|
||||||
mut config: ServerConfig,
|
mut config: ServerConfig,
|
||||||
@@ -161,32 +175,34 @@ mod rustls {
|
|||||||
Error = TlsError<io::Error, DispatchError>,
|
Error = TlsError<io::Error, DispatchError>,
|
||||||
InitError = S::InitError,
|
InitError = S::InitError,
|
||||||
> {
|
> {
|
||||||
let protos = vec!["h2".to_string().into()];
|
let mut protos = vec![b"h2".to_vec()];
|
||||||
config.set_protocols(&protos);
|
protos.extend_from_slice(&config.alpn_protocols);
|
||||||
|
config.alpn_protocols = protos;
|
||||||
|
|
||||||
pipeline_factory(
|
Acceptor::new(config)
|
||||||
Acceptor::new(config)
|
.map_init_err(|_| {
|
||||||
.map_err(TlsError::Tls)
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
.map_init_err(|_| panic!()),
|
})
|
||||||
)
|
.map_err(TlsError::into_service_error)
|
||||||
.and_then(fn_factory(|| {
|
.map(|io: TlsStream<TcpStream>| {
|
||||||
ok::<_, S::InitError>(fn_service(|io: TlsStream<TcpStream>| {
|
|
||||||
let peer_addr = io.get_ref().0.peer_addr().ok();
|
let peer_addr = io.get_ref().0.peer_addr().ok();
|
||||||
ok((io, peer_addr))
|
(io, peer_addr)
|
||||||
}))
|
})
|
||||||
}))
|
.and_then(self.map_err(TlsError::Service))
|
||||||
.and_then(self.map_err(TlsError::Service))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
|
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
|
||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||||
|
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
S: ServiceFactory<Request, Config = ()>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Future: 'static,
|
||||||
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
<S::Service as Service<Request>>::Future: 'static,
|
||||||
|
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
type Response = ();
|
type Response = ();
|
||||||
@@ -194,62 +210,26 @@ where
|
|||||||
type Config = ();
|
type Config = ();
|
||||||
type Service = H2ServiceHandler<T, S::Service, B>;
|
type Service = H2ServiceHandler<T, S::Service, B>;
|
||||||
type InitError = S::InitError;
|
type InitError = S::InitError;
|
||||||
type Future = H2ServiceResponse<T, S, B>;
|
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||||
|
|
||||||
fn new_service(&self, _: ()) -> Self::Future {
|
fn new_service(&self, _: ()) -> Self::Future {
|
||||||
H2ServiceResponse {
|
let service = self.srv.new_service(());
|
||||||
fut: self.srv.new_service(()),
|
let cfg = self.cfg.clone();
|
||||||
cfg: Some(self.cfg.clone()),
|
let on_connect_ext = self.on_connect_ext.clone();
|
||||||
on_connect_ext: self.on_connect_ext.clone(),
|
|
||||||
_phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
Box::pin(async move {
|
||||||
#[pin_project::pin_project]
|
let service = service.await?;
|
||||||
pub struct H2ServiceResponse<T, S, B>
|
Ok(H2ServiceHandler::new(cfg, on_connect_ext, service))
|
||||||
where
|
|
||||||
S: ServiceFactory<Request>,
|
|
||||||
{
|
|
||||||
#[pin]
|
|
||||||
fut: S::Future,
|
|
||||||
cfg: Option<ServiceConfig>,
|
|
||||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
|
||||||
_phantom: PhantomData<B>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, S, B> Future for H2ServiceResponse<T, S, B>
|
|
||||||
where
|
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
|
||||||
S: ServiceFactory<Request, Config = ()>,
|
|
||||||
S::Error: Into<Error> + 'static,
|
|
||||||
S::Response: Into<Response<B>> + 'static,
|
|
||||||
<S::Service as Service<Request>>::Future: 'static,
|
|
||||||
B: MessageBody + 'static,
|
|
||||||
{
|
|
||||||
type Output = Result<H2ServiceHandler<T, S::Service, B>, S::InitError>;
|
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
||||||
let this = self.as_mut().project();
|
|
||||||
|
|
||||||
this.fut.poll(cx).map_ok(|service| {
|
|
||||||
let this = self.as_mut().project();
|
|
||||||
H2ServiceHandler::new(
|
|
||||||
this.cfg.take().unwrap(),
|
|
||||||
this.on_connect_ext.clone(),
|
|
||||||
service,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Service` implementation for http/2 transport
|
/// `Service` implementation for HTTP/2 transport
|
||||||
pub struct H2ServiceHandler<T, S, B>
|
pub struct H2ServiceHandler<T, S, B>
|
||||||
where
|
where
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
{
|
{
|
||||||
flow: Rc<RefCell<HttpFlow<S, (), ()>>>,
|
flow: Rc<HttpFlow<S, (), ()>>,
|
||||||
cfg: ServiceConfig,
|
cfg: ServiceConfig,
|
||||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||||
_phantom: PhantomData<B>,
|
_phantom: PhantomData<B>,
|
||||||
@@ -258,7 +238,7 @@ where
|
|||||||
impl<T, S, B> H2ServiceHandler<T, S, B>
|
impl<T, S, B> H2ServiceHandler<T, S, B>
|
||||||
where
|
where
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
@@ -281,7 +261,7 @@ impl<T, S, B> Service<(T, Option<net::SocketAddr>)> for H2ServiceHandler<T, S, B
|
|||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
@@ -290,15 +270,15 @@ where
|
|||||||
type Error = DispatchError;
|
type Error = DispatchError;
|
||||||
type Future = H2ServiceHandlerResponse<T, S, B>;
|
type Future = H2ServiceHandlerResponse<T, S, B>;
|
||||||
|
|
||||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self.flow.borrow_mut().service.poll_ready(cx).map_err(|e| {
|
self.flow.service.poll_ready(cx).map_err(|e| {
|
||||||
let e = e.into();
|
let e = e.into();
|
||||||
error!("Service readiness error: {:?}", e);
|
error!("Service readiness error: {:?}", e);
|
||||||
DispatchError::Service(e)
|
DispatchError::Service(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call(&mut self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||||
let on_connect_data =
|
let on_connect_data =
|
||||||
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||||
|
|
||||||
@@ -308,7 +288,7 @@ where
|
|||||||
Some(self.cfg.clone()),
|
Some(self.cfg.clone()),
|
||||||
addr,
|
addr,
|
||||||
on_connect_data,
|
on_connect_data,
|
||||||
server::handshake(io),
|
handshake_with_timeout(io, &self.cfg),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,11 +301,11 @@ where
|
|||||||
{
|
{
|
||||||
Incoming(Dispatcher<T, S, B, (), ()>),
|
Incoming(Dispatcher<T, S, B, (), ()>),
|
||||||
Handshake(
|
Handshake(
|
||||||
Option<Rc<RefCell<HttpFlow<S, (), ()>>>>,
|
Option<Rc<HttpFlow<S, (), ()>>>,
|
||||||
Option<ServiceConfig>,
|
Option<ServiceConfig>,
|
||||||
Option<net::SocketAddr>,
|
Option<net::SocketAddr>,
|
||||||
OnConnectData,
|
OnConnectData,
|
||||||
Handshake<T, Bytes>,
|
HandshakeWithTimeout<T>,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +313,7 @@ pub struct H2ServiceHandlerResponse<T, S, B>
|
|||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
@@ -345,7 +325,7 @@ impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
|
|||||||
where
|
where
|
||||||
T: AsyncRead + AsyncWrite + Unpin,
|
T: AsyncRead + AsyncWrite + Unpin,
|
||||||
S: Service<Request>,
|
S: Service<Request>,
|
||||||
S::Error: Into<Error> + 'static,
|
S::Error: Into<Response<BoxBody>> + 'static,
|
||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
S::Response: Into<Response<B>> + 'static,
|
S::Response: Into<Response<B>> + 'static,
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
@@ -362,21 +342,21 @@ where
|
|||||||
ref mut on_connect_data,
|
ref mut on_connect_data,
|
||||||
ref mut handshake,
|
ref mut handshake,
|
||||||
) => match ready!(Pin::new(handshake).poll(cx)) {
|
) => match ready!(Pin::new(handshake).poll(cx)) {
|
||||||
Ok(conn) => {
|
Ok((conn, timer)) => {
|
||||||
let on_connect_data = std::mem::take(on_connect_data);
|
let on_connect_data = std::mem::take(on_connect_data);
|
||||||
self.state = State::Incoming(Dispatcher::new(
|
self.state = State::Incoming(Dispatcher::new(
|
||||||
srv.take().unwrap(),
|
srv.take().unwrap(),
|
||||||
conn,
|
conn,
|
||||||
on_connect_data,
|
on_connect_data,
|
||||||
config.take().unwrap(),
|
config.take().unwrap(),
|
||||||
None,
|
|
||||||
*peer_addr,
|
*peer_addr,
|
||||||
|
timer,
|
||||||
));
|
));
|
||||||
self.poll(cx)
|
self.poll(cx)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("H2 handshake error: {}", err);
|
trace!("H2 handshake error: {}", err);
|
||||||
Poll::Ready(Err(err.into()))
|
Poll::Ready(Err(err))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
51
actix-http/src/header/as_name.rs
Normal file
51
actix-http/src/header/as_name.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
//! Sealed [`AsHeaderName`] trait and implementations.
|
||||||
|
|
||||||
|
use std::{borrow::Cow, str::FromStr as _};
|
||||||
|
|
||||||
|
use http::header::{HeaderName, InvalidHeaderName};
|
||||||
|
|
||||||
|
/// Sealed trait implemented for types that can be effectively borrowed as a [`HeaderValue`].
|
||||||
|
///
|
||||||
|
/// [`HeaderValue`]: crate::http::HeaderValue
|
||||||
|
pub trait AsHeaderName: Sealed {}
|
||||||
|
|
||||||
|
pub struct Seal;
|
||||||
|
|
||||||
|
pub trait Sealed {
|
||||||
|
fn try_as_name(&self, seal: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for HeaderName {
|
||||||
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
|
Ok(Cow::Borrowed(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AsHeaderName for HeaderName {}
|
||||||
|
|
||||||
|
impl Sealed for &HeaderName {
|
||||||
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
|
Ok(Cow::Borrowed(*self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AsHeaderName for &HeaderName {}
|
||||||
|
|
||||||
|
impl Sealed for &str {
|
||||||
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
|
HeaderName::from_str(self).map(Cow::Owned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AsHeaderName for &str {}
|
||||||
|
|
||||||
|
impl Sealed for String {
|
||||||
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
|
HeaderName::from_str(self).map(Cow::Owned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AsHeaderName for String {}
|
||||||
|
|
||||||
|
impl Sealed for &String {
|
||||||
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
|
HeaderName::from_str(self).map(Cow::Owned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AsHeaderName for &String {}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
use crate::header::{Charset, QualityItem, ACCEPT_CHARSET};
|
|
||||||
|
|
||||||
header! {
|
|
||||||
/// `Accept-Charset` header, defined in
|
|
||||||
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3)
|
|
||||||
///
|
|
||||||
/// The `Accept-Charset` header field can be sent by a user agent to
|
|
||||||
/// indicate what charsets are acceptable in textual response content.
|
|
||||||
/// This field allows user agents capable of understanding more
|
|
||||||
/// comprehensive or special-purpose charsets to signal that capability
|
|
||||||
/// to an origin server that is capable of representing information in
|
|
||||||
/// those charsets.
|
|
||||||
///
|
|
||||||
/// # ABNF
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// Accept-Charset = 1#( ( charset / "*" ) [ weight ] )
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Example values
|
|
||||||
/// * `iso-8859-5, unicode-1-1;q=0.8`
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
/// # extern crate actix_http;
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// use actix_http::http::header::{AcceptCharset, Charset, qitem};
|
|
||||||
///
|
|
||||||
/// # fn main() {
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// builder.set(
|
|
||||||
/// AcceptCharset(vec![qitem(Charset::Us_Ascii)])
|
|
||||||
/// );
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
/// ```rust
|
|
||||||
/// # extern crate actix_http;
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// use actix_http::http::header::{AcceptCharset, Charset, q, QualityItem};
|
|
||||||
///
|
|
||||||
/// # fn main() {
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// builder.set(
|
|
||||||
/// AcceptCharset(vec![
|
|
||||||
/// QualityItem::new(Charset::Us_Ascii, q(900)),
|
|
||||||
/// QualityItem::new(Charset::Iso_8859_10, q(200)),
|
|
||||||
/// ])
|
|
||||||
/// );
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
/// ```rust
|
|
||||||
/// # extern crate actix_http;
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// use actix_http::http::header::{AcceptCharset, Charset, qitem};
|
|
||||||
///
|
|
||||||
/// # fn main() {
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// builder.set(
|
|
||||||
/// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))])
|
|
||||||
/// );
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+
|
|
||||||
|
|
||||||
test_accept_charset {
|
|
||||||
// Test case from RFC
|
|
||||||
test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
use header::{Encoding, QualityItem};
|
|
||||||
|
|
||||||
header! {
|
|
||||||
/// `Accept-Encoding` header, defined in
|
|
||||||
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4)
|
|
||||||
///
|
|
||||||
/// The `Accept-Encoding` header field can be used by user agents to
|
|
||||||
/// indicate what response content-codings are
|
|
||||||
/// acceptable in the response. An `identity` token is used as a synonym
|
|
||||||
/// for "no encoding" in order to communicate when no encoding is
|
|
||||||
/// preferred.
|
|
||||||
///
|
|
||||||
/// # ABNF
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// Accept-Encoding = #( codings [ weight ] )
|
|
||||||
/// codings = content-coding / "identity" / "*"
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Example values
|
|
||||||
/// * `compress, gzip`
|
|
||||||
/// * ``
|
|
||||||
/// * `*`
|
|
||||||
/// * `compress;q=0.5, gzip;q=1`
|
|
||||||
/// * `gzip;q=1.0, identity; q=0.5, *;q=0`
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem};
|
|
||||||
///
|
|
||||||
/// let mut headers = Headers::new();
|
|
||||||
/// headers.set(
|
|
||||||
/// AcceptEncoding(vec![qitem(Encoding::Chunked)])
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
/// ```
|
|
||||||
/// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem};
|
|
||||||
///
|
|
||||||
/// let mut headers = Headers::new();
|
|
||||||
/// headers.set(
|
|
||||||
/// AcceptEncoding(vec![
|
|
||||||
/// qitem(Encoding::Chunked),
|
|
||||||
/// qitem(Encoding::Gzip),
|
|
||||||
/// qitem(Encoding::Deflate),
|
|
||||||
/// ])
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
/// ```
|
|
||||||
/// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem};
|
|
||||||
///
|
|
||||||
/// let mut headers = Headers::new();
|
|
||||||
/// headers.set(
|
|
||||||
/// AcceptEncoding(vec![
|
|
||||||
/// qitem(Encoding::Chunked),
|
|
||||||
/// QualityItem::new(Encoding::Gzip, q(600)),
|
|
||||||
/// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)),
|
|
||||||
/// ])
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
(AcceptEncoding, "Accept-Encoding") => (QualityItem<Encoding>)*
|
|
||||||
|
|
||||||
test_accept_encoding {
|
|
||||||
// From the RFC
|
|
||||||
test_header!(test1, vec![b"compress, gzip"]);
|
|
||||||
test_header!(test2, vec![b""], Some(AcceptEncoding(vec![])));
|
|
||||||
test_header!(test3, vec![b"*"]);
|
|
||||||
// Note: Removed quality 1 from gzip
|
|
||||||
test_header!(test4, vec![b"compress;q=0.5, gzip"]);
|
|
||||||
// Note: Removed quality 1 from gzip
|
|
||||||
test_header!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
use crate::header::{QualityItem, ACCEPT_LANGUAGE};
|
|
||||||
use language_tags::LanguageTag;
|
|
||||||
|
|
||||||
header! {
|
|
||||||
/// `Accept-Language` header, defined in
|
|
||||||
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5)
|
|
||||||
///
|
|
||||||
/// The `Accept-Language` header field can be used by user agents to
|
|
||||||
/// indicate the set of natural languages that are preferred in the
|
|
||||||
/// response.
|
|
||||||
///
|
|
||||||
/// # ABNF
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// Accept-Language = 1#( language-range [ weight ] )
|
|
||||||
/// language-range = <language-range, see [RFC4647], Section 2.1>
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Example values
|
|
||||||
/// * `da, en-gb;q=0.8, en;q=0.7`
|
|
||||||
/// * `en-us;q=1.0, en;q=0.5, fr`
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # extern crate actix_http;
|
|
||||||
/// # extern crate language_tags;
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// use actix_http::http::header::{AcceptLanguage, LanguageTag, qitem};
|
|
||||||
///
|
|
||||||
/// # fn main() {
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// let mut langtag: LanguageTag = Default::default();
|
|
||||||
/// langtag.language = Some("en".to_owned());
|
|
||||||
/// langtag.region = Some("US".to_owned());
|
|
||||||
/// builder.set(
|
|
||||||
/// AcceptLanguage(vec![
|
|
||||||
/// qitem(langtag),
|
|
||||||
/// ])
|
|
||||||
/// );
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # extern crate actix_http;
|
|
||||||
/// # #[macro_use] extern crate language_tags;
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// use actix_http::http::header::{AcceptLanguage, QualityItem, q, qitem};
|
|
||||||
/// #
|
|
||||||
/// # fn main() {
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// builder.set(
|
|
||||||
/// AcceptLanguage(vec![
|
|
||||||
/// qitem(langtag!(da)),
|
|
||||||
/// QualityItem::new(langtag!(en;;;GB), q(800)),
|
|
||||||
/// QualityItem::new(langtag!(en), q(700)),
|
|
||||||
/// ])
|
|
||||||
/// );
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
(AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem<LanguageTag>)+
|
|
||||||
|
|
||||||
test_accept_language {
|
|
||||||
// From the RFC
|
|
||||||
test_header!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]);
|
|
||||||
// Own test
|
|
||||||
test_header!(
|
|
||||||
test2, vec![b"en-US, en; q=0.5, fr"],
|
|
||||||
Some(AcceptLanguage(vec![
|
|
||||||
qitem("en-US".parse().unwrap()),
|
|
||||||
QualityItem::new("en".parse().unwrap(), q(500)),
|
|
||||||
qitem("fr".parse().unwrap()),
|
|
||||||
])));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
use std::fmt::{self, Write};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use http::header;
|
|
||||||
|
|
||||||
use crate::header::{
|
|
||||||
fmt_comma_delimited, from_comma_delimited, Header, IntoHeaderValue, Writer,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2)
|
|
||||||
///
|
|
||||||
/// The `Cache-Control` header field is used to specify directives for
|
|
||||||
/// caches along the request/response chain. Such cache directives are
|
|
||||||
/// unidirectional in that the presence of a directive in a request does
|
|
||||||
/// not imply that the same directive is to be given in the response.
|
|
||||||
///
|
|
||||||
/// # ABNF
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// Cache-Control = 1#cache-directive
|
|
||||||
/// cache-directive = token [ "=" ( token / quoted-string ) ]
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Example values
|
|
||||||
///
|
|
||||||
/// * `no-cache`
|
|
||||||
/// * `private, community="UCI"`
|
|
||||||
/// * `max-age=30`
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// use actix_http::http::header::{CacheControl, CacheDirective};
|
|
||||||
///
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// use actix_http::http::header::{CacheControl, CacheDirective};
|
|
||||||
///
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// builder.set(CacheControl(vec![
|
|
||||||
/// CacheDirective::NoCache,
|
|
||||||
/// CacheDirective::Private,
|
|
||||||
/// CacheDirective::MaxAge(360u32),
|
|
||||||
/// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())),
|
|
||||||
/// ]));
|
|
||||||
/// ```
|
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
|
||||||
pub struct CacheControl(pub Vec<CacheDirective>);
|
|
||||||
|
|
||||||
__hyper__deref!(CacheControl => Vec<CacheDirective>);
|
|
||||||
|
|
||||||
//TODO: this could just be the header! macro
|
|
||||||
impl Header for CacheControl {
|
|
||||||
fn name() -> header::HeaderName {
|
|
||||||
header::CACHE_CONTROL
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn parse<T>(msg: &T) -> Result<Self, crate::error::ParseError>
|
|
||||||
where
|
|
||||||
T: crate::HttpMessage,
|
|
||||||
{
|
|
||||||
let directives = from_comma_delimited(msg.headers().get_all(&Self::name()))?;
|
|
||||||
if !directives.is_empty() {
|
|
||||||
Ok(CacheControl(directives))
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ParseError::Header)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CacheControl {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt_comma_delimited(f, &self[..])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoHeaderValue for CacheControl {
|
|
||||||
type Error = header::InvalidHeaderValue;
|
|
||||||
|
|
||||||
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
|
|
||||||
let mut writer = Writer::new();
|
|
||||||
let _ = write!(&mut writer, "{}", self);
|
|
||||||
header::HeaderValue::from_maybe_shared(writer.take())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `CacheControl` contains a list of these directives.
|
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
|
||||||
pub enum CacheDirective {
|
|
||||||
/// "no-cache"
|
|
||||||
NoCache,
|
|
||||||
/// "no-store"
|
|
||||||
NoStore,
|
|
||||||
/// "no-transform"
|
|
||||||
NoTransform,
|
|
||||||
/// "only-if-cached"
|
|
||||||
OnlyIfCached,
|
|
||||||
|
|
||||||
// request directives
|
|
||||||
/// "max-age=delta"
|
|
||||||
MaxAge(u32),
|
|
||||||
/// "max-stale=delta"
|
|
||||||
MaxStale(u32),
|
|
||||||
/// "min-fresh=delta"
|
|
||||||
MinFresh(u32),
|
|
||||||
|
|
||||||
// response directives
|
|
||||||
/// "must-revalidate"
|
|
||||||
MustRevalidate,
|
|
||||||
/// "public"
|
|
||||||
Public,
|
|
||||||
/// "private"
|
|
||||||
Private,
|
|
||||||
/// "proxy-revalidate"
|
|
||||||
ProxyRevalidate,
|
|
||||||
/// "s-maxage=delta"
|
|
||||||
SMaxAge(u32),
|
|
||||||
|
|
||||||
/// Extension directives. Optionally include an argument.
|
|
||||||
Extension(String, Option<String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CacheDirective {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
use self::CacheDirective::*;
|
|
||||||
fmt::Display::fmt(
|
|
||||||
match *self {
|
|
||||||
NoCache => "no-cache",
|
|
||||||
NoStore => "no-store",
|
|
||||||
NoTransform => "no-transform",
|
|
||||||
OnlyIfCached => "only-if-cached",
|
|
||||||
|
|
||||||
MaxAge(secs) => return write!(f, "max-age={}", secs),
|
|
||||||
MaxStale(secs) => return write!(f, "max-stale={}", secs),
|
|
||||||
MinFresh(secs) => return write!(f, "min-fresh={}", secs),
|
|
||||||
|
|
||||||
MustRevalidate => "must-revalidate",
|
|
||||||
Public => "public",
|
|
||||||
Private => "private",
|
|
||||||
ProxyRevalidate => "proxy-revalidate",
|
|
||||||
SMaxAge(secs) => return write!(f, "s-maxage={}", secs),
|
|
||||||
|
|
||||||
Extension(ref name, None) => &name[..],
|
|
||||||
Extension(ref name, Some(ref arg)) => {
|
|
||||||
return write!(f, "{}={}", name, arg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for CacheDirective {
|
|
||||||
type Err = Option<<u32 as FromStr>::Err>;
|
|
||||||
fn from_str(s: &str) -> Result<CacheDirective, Option<<u32 as FromStr>::Err>> {
|
|
||||||
use self::CacheDirective::*;
|
|
||||||
match s {
|
|
||||||
"no-cache" => Ok(NoCache),
|
|
||||||
"no-store" => Ok(NoStore),
|
|
||||||
"no-transform" => Ok(NoTransform),
|
|
||||||
"only-if-cached" => Ok(OnlyIfCached),
|
|
||||||
"must-revalidate" => Ok(MustRevalidate),
|
|
||||||
"public" => Ok(Public),
|
|
||||||
"private" => Ok(Private),
|
|
||||||
"proxy-revalidate" => Ok(ProxyRevalidate),
|
|
||||||
"" => Err(None),
|
|
||||||
_ => match s.find('=') {
|
|
||||||
Some(idx) if idx + 1 < s.len() => {
|
|
||||||
match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) {
|
|
||||||
("max-age", secs) => secs.parse().map(MaxAge).map_err(Some),
|
|
||||||
("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some),
|
|
||||||
("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some),
|
|
||||||
("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some),
|
|
||||||
(left, right) => {
|
|
||||||
Ok(Extension(left.to_owned(), Some(right.to_owned())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(_) => Err(None),
|
|
||||||
None => Ok(Extension(s.to_owned(), None)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::header::Header;
|
|
||||||
use crate::test::TestRequest;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_multiple_headers() {
|
|
||||||
let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private")
|
|
||||||
.finish();
|
|
||||||
let cache = Header::parse(&req);
|
|
||||||
assert_eq!(
|
|
||||||
cache.ok(),
|
|
||||||
Some(CacheControl(vec![
|
|
||||||
CacheDirective::NoCache,
|
|
||||||
CacheDirective::Private,
|
|
||||||
]))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_argument() {
|
|
||||||
let req =
|
|
||||||
TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private")
|
|
||||||
.finish();
|
|
||||||
let cache = Header::parse(&req);
|
|
||||||
assert_eq!(
|
|
||||||
cache.ok(),
|
|
||||||
Some(CacheControl(vec![
|
|
||||||
CacheDirective::MaxAge(100),
|
|
||||||
CacheDirective::Private,
|
|
||||||
]))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_quote_form() {
|
|
||||||
let req =
|
|
||||||
TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish();
|
|
||||||
let cache = Header::parse(&req);
|
|
||||||
assert_eq!(
|
|
||||||
cache.ok(),
|
|
||||||
Some(CacheControl(vec![CacheDirective::MaxAge(200)]))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_extension() {
|
|
||||||
let req =
|
|
||||||
TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish();
|
|
||||||
let cache = Header::parse(&req);
|
|
||||||
assert_eq!(
|
|
||||||
cache.ok(),
|
|
||||||
Some(CacheControl(vec![
|
|
||||||
CacheDirective::Extension("foo".to_owned(), None),
|
|
||||||
CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())),
|
|
||||||
]))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_bad_syntax() {
|
|
||||||
let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish();
|
|
||||||
let cache: Result<CacheControl, _> = Header::parse(&req);
|
|
||||||
assert_eq!(cache.ok(), None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
use crate::header::{QualityItem, CONTENT_LANGUAGE};
|
|
||||||
use language_tags::LanguageTag;
|
|
||||||
|
|
||||||
header! {
|
|
||||||
/// `Content-Language` header, defined in
|
|
||||||
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2)
|
|
||||||
///
|
|
||||||
/// The `Content-Language` header field describes the natural language(s)
|
|
||||||
/// of the intended audience for the representation. Note that this
|
|
||||||
/// might not be equivalent to all the languages used within the
|
|
||||||
/// representation.
|
|
||||||
///
|
|
||||||
/// # ABNF
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// Content-Language = 1#language-tag
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Example values
|
|
||||||
///
|
|
||||||
/// * `da`
|
|
||||||
/// * `mi, en`
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # extern crate actix_http;
|
|
||||||
/// # #[macro_use] extern crate language_tags;
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// # use actix_http::http::header::{ContentLanguage, qitem};
|
|
||||||
/// #
|
|
||||||
/// # fn main() {
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// builder.set(
|
|
||||||
/// ContentLanguage(vec![
|
|
||||||
/// qitem(langtag!(en)),
|
|
||||||
/// ])
|
|
||||||
/// );
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # extern crate actix_http;
|
|
||||||
/// # #[macro_use] extern crate language_tags;
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// # use actix_http::http::header::{ContentLanguage, qitem};
|
|
||||||
/// #
|
|
||||||
/// # fn main() {
|
|
||||||
///
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// builder.set(
|
|
||||||
/// ContentLanguage(vec![
|
|
||||||
/// qitem(langtag!(da)),
|
|
||||||
/// qitem(langtag!(en;;;GB)),
|
|
||||||
/// ])
|
|
||||||
/// );
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
(ContentLanguage, CONTENT_LANGUAGE) => (QualityItem<LanguageTag>)+
|
|
||||||
|
|
||||||
test_content_language {
|
|
||||||
test_header!(test1, vec![b"da"]);
|
|
||||||
test_header!(test2, vec![b"mi, en"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
use crate::header::{HttpDate, DATE};
|
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
header! {
|
|
||||||
/// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2)
|
|
||||||
///
|
|
||||||
/// The `Date` header field represents the date and time at which the
|
|
||||||
/// message was originated.
|
|
||||||
///
|
|
||||||
/// # ABNF
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// Date = HTTP-date
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Example values
|
|
||||||
///
|
|
||||||
/// * `Tue, 15 Nov 1994 08:12:31 GMT`
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// use actix_http::http::header::Date;
|
|
||||||
/// use std::time::SystemTime;
|
|
||||||
///
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// builder.set(Date(SystemTime::now().into()));
|
|
||||||
/// ```
|
|
||||||
(Date, DATE) => [HttpDate]
|
|
||||||
|
|
||||||
test_date {
|
|
||||||
test_header!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Date {
|
|
||||||
/// Create a date instance set to the current system time
|
|
||||||
pub fn now() -> Date {
|
|
||||||
Date(SystemTime::now().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
use crate::header::{HttpDate, LAST_MODIFIED};
|
|
||||||
|
|
||||||
header! {
|
|
||||||
/// `Last-Modified` header, defined in
|
|
||||||
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2)
|
|
||||||
///
|
|
||||||
/// The `Last-Modified` header field in a response provides a timestamp
|
|
||||||
/// indicating the date and time at which the origin server believes the
|
|
||||||
/// selected representation was last modified, as determined at the
|
|
||||||
/// conclusion of handling the request.
|
|
||||||
///
|
|
||||||
/// # ABNF
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// Expires = HTTP-date
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Example values
|
|
||||||
///
|
|
||||||
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use actix_http::Response;
|
|
||||||
/// use actix_http::http::header::LastModified;
|
|
||||||
/// use std::time::{SystemTime, Duration};
|
|
||||||
///
|
|
||||||
/// let mut builder = Response::Ok();
|
|
||||||
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
|
|
||||||
/// builder.set(LastModified(modified.into()));
|
|
||||||
/// ```
|
|
||||||
(LastModified, LAST_MODIFIED) => [HttpDate]
|
|
||||||
|
|
||||||
test_last_modified {
|
|
||||||
// Test case from RFC
|
|
||||||
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);}
|
|
||||||
}
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
//! A Collection of Header implementations for common HTTP Headers.
|
|
||||||
//!
|
|
||||||
//! ## Mime
|
|
||||||
//!
|
|
||||||
//! Several header fields use MIME values for their contents. Keeping with the
|
|
||||||
//! strongly-typed theme, the [mime] 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::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, DispositionParam, DispositionType,
|
|
||||||
};
|
|
||||||
pub use self::content_language::ContentLanguage;
|
|
||||||
pub use self::content_range::{ContentRange, ContentRangeSpec};
|
|
||||||
pub use self::content_type::ContentType;
|
|
||||||
pub use self::date::Date;
|
|
||||||
pub use self::etag::ETag;
|
|
||||||
pub use self::expires::Expires;
|
|
||||||
pub use self::if_match::IfMatch;
|
|
||||||
pub use self::if_modified_since::IfModifiedSince;
|
|
||||||
pub use self::if_none_match::IfNoneMatch;
|
|
||||||
pub use self::if_range::IfRange;
|
|
||||||
pub use self::if_unmodified_since::IfUnmodifiedSince;
|
|
||||||
pub use self::last_modified::LastModified;
|
|
||||||
//pub use self::range::{Range, ByteRangeSpec};
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! __hyper__deref {
|
|
||||||
($from:ty => $to:ty) => {
|
|
||||||
impl ::std::ops::Deref for $from {
|
|
||||||
type Target = $to;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn deref(&self) -> &$to {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::std::ops::DerefMut for $from {
|
|
||||||
#[inline]
|
|
||||||
fn deref_mut(&mut self) -> &mut $to {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! __hyper__tm {
|
|
||||||
($id:ident, $tm:ident{$($tf:item)*}) => {
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
#[cfg(test)]
|
|
||||||
mod $tm{
|
|
||||||
use std::str;
|
|
||||||
use http::Method;
|
|
||||||
use mime::*;
|
|
||||||
use $crate::header::*;
|
|
||||||
use super::$id as HeaderField;
|
|
||||||
$($tf)*
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! test_header {
|
|
||||||
($id:ident, $raw:expr) => {
|
|
||||||
#[test]
|
|
||||||
fn $id() {
|
|
||||||
use super::*;
|
|
||||||
use $crate::test;
|
|
||||||
|
|
||||||
let raw = $raw;
|
|
||||||
let a: Vec<Vec<u8>> = raw.iter().map(|x| x.to_vec()).collect();
|
|
||||||
let mut req = test::TestRequest::default();
|
|
||||||
for item in a {
|
|
||||||
req = req.header(HeaderField::name(), item).take();
|
|
||||||
}
|
|
||||||
let req = req.finish();
|
|
||||||
let value = HeaderField::parse(&req);
|
|
||||||
let result = format!("{}", value.unwrap());
|
|
||||||
let expected = String::from_utf8(raw[0].to_vec()).unwrap();
|
|
||||||
let result_cmp: Vec<String> = result
|
|
||||||
.to_ascii_lowercase()
|
|
||||||
.split(' ')
|
|
||||||
.map(|x| x.to_owned())
|
|
||||||
.collect();
|
|
||||||
let expected_cmp: Vec<String> = expected
|
|
||||||
.to_ascii_lowercase()
|
|
||||||
.split(' ')
|
|
||||||
.map(|x| x.to_owned())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(result_cmp.concat(), expected_cmp.concat());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($id:ident, $raw:expr, $typed:expr) => {
|
|
||||||
#[test]
|
|
||||||
fn $id() {
|
|
||||||
use $crate::test;
|
|
||||||
|
|
||||||
let a: Vec<Vec<u8>> = $raw.iter().map(|x| x.to_vec()).collect();
|
|
||||||
let mut req = test::TestRequest::default();
|
|
||||||
for item in a {
|
|
||||||
req.header(HeaderField::name(), item);
|
|
||||||
}
|
|
||||||
let req = req.finish();
|
|
||||||
let val = HeaderField::parse(&req);
|
|
||||||
let typed: Option<HeaderField> = $typed;
|
|
||||||
// Test parsing
|
|
||||||
assert_eq!(val.ok(), typed);
|
|
||||||
// Test formatting
|
|
||||||
if typed.is_some() {
|
|
||||||
let raw = &($raw)[..];
|
|
||||||
let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap());
|
|
||||||
let mut joined = String::new();
|
|
||||||
joined.push_str(iter.next().unwrap());
|
|
||||||
for s in iter {
|
|
||||||
joined.push_str(", ");
|
|
||||||
joined.push_str(s);
|
|
||||||
}
|
|
||||||
assert_eq!(format!("{}", typed.unwrap()), joined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! header {
|
|
||||||
// $a:meta: Attributes associated with the header item (usually docs)
|
|
||||||
// $id:ident: Identifier of the header
|
|
||||||
// $n:expr: Lowercase name of the header
|
|
||||||
// $nn:expr: Nice name of the header
|
|
||||||
|
|
||||||
// List header, zero or more items
|
|
||||||
($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)*) => {
|
|
||||||
$(#[$a])*
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct $id(pub Vec<$item>);
|
|
||||||
__hyper__deref!($id => Vec<$item>);
|
|
||||||
impl $crate::http::header::Header for $id {
|
|
||||||
#[inline]
|
|
||||||
fn name() -> $crate::http::header::HeaderName {
|
|
||||||
$name
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn parse<T>(msg: &T) -> Result<Self, $crate::error::ParseError>
|
|
||||||
where T: $crate::HttpMessage
|
|
||||||
{
|
|
||||||
$crate::http::header::from_comma_delimited(
|
|
||||||
msg.headers().get_all(Self::name())).map($id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::fmt::Display for $id {
|
|
||||||
#[inline]
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
|
||||||
$crate::http::header::fmt_comma_delimited(f, &self.0[..])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl $crate::http::header::IntoHeaderValue for $id {
|
|
||||||
type Error = $crate::http::header::InvalidHeaderValue;
|
|
||||||
|
|
||||||
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
|
||||||
use std::fmt::Write;
|
|
||||||
let mut writer = $crate::http::header::Writer::new();
|
|
||||||
let _ = write!(&mut writer, "{}", self);
|
|
||||||
$crate::http::header::HeaderValue::from_maybe_shared(writer.take())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// List header, one or more items
|
|
||||||
($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => {
|
|
||||||
$(#[$a])*
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct $id(pub Vec<$item>);
|
|
||||||
__hyper__deref!($id => Vec<$item>);
|
|
||||||
impl $crate::http::header::Header for $id {
|
|
||||||
#[inline]
|
|
||||||
fn name() -> $crate::http::header::HeaderName {
|
|
||||||
$name
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn parse<T>(msg: &T) -> Result<Self, $crate::error::ParseError>
|
|
||||||
where T: $crate::HttpMessage
|
|
||||||
{
|
|
||||||
$crate::http::header::from_comma_delimited(
|
|
||||||
msg.headers().get_all(Self::name())).map($id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::fmt::Display for $id {
|
|
||||||
#[inline]
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
$crate::http::header::fmt_comma_delimited(f, &self.0[..])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl $crate::http::header::IntoHeaderValue for $id {
|
|
||||||
type Error = $crate::http::header::InvalidHeaderValue;
|
|
||||||
|
|
||||||
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
|
||||||
use std::fmt::Write;
|
|
||||||
let mut writer = $crate::http::header::Writer::new();
|
|
||||||
let _ = write!(&mut writer, "{}", self);
|
|
||||||
$crate::http::header::HeaderValue::from_maybe_shared(writer.take())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Single value header
|
|
||||||
($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => {
|
|
||||||
$(#[$a])*
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct $id(pub $value);
|
|
||||||
__hyper__deref!($id => $value);
|
|
||||||
impl $crate::http::header::Header for $id {
|
|
||||||
#[inline]
|
|
||||||
fn name() -> $crate::http::header::HeaderName {
|
|
||||||
$name
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn parse<T>(msg: &T) -> Result<Self, $crate::error::ParseError>
|
|
||||||
where T: $crate::HttpMessage
|
|
||||||
{
|
|
||||||
$crate::http::header::from_one_raw_str(
|
|
||||||
msg.headers().get(Self::name())).map($id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::fmt::Display for $id {
|
|
||||||
#[inline]
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
std::fmt::Display::fmt(&self.0, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl $crate::http::header::IntoHeaderValue for $id {
|
|
||||||
type Error = $crate::http::header::InvalidHeaderValue;
|
|
||||||
|
|
||||||
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
|
||||||
self.0.try_into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// List header, one or more items with "*" option
|
|
||||||
($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => {
|
|
||||||
$(#[$a])*
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum $id {
|
|
||||||
/// Any value is a match
|
|
||||||
Any,
|
|
||||||
/// Only the listed items are a match
|
|
||||||
Items(Vec<$item>),
|
|
||||||
}
|
|
||||||
impl $crate::http::header::Header for $id {
|
|
||||||
#[inline]
|
|
||||||
fn name() -> $crate::http::header::HeaderName {
|
|
||||||
$name
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn parse<T>(msg: &T) -> Result<Self, $crate::error::ParseError>
|
|
||||||
where T: $crate::HttpMessage
|
|
||||||
{
|
|
||||||
let any = msg.headers().get(Self::name()).and_then(|hdr| {
|
|
||||||
hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))});
|
|
||||||
|
|
||||||
if let Some(true) = any {
|
|
||||||
Ok($id::Any)
|
|
||||||
} else {
|
|
||||||
Ok($id::Items(
|
|
||||||
$crate::http::header::from_comma_delimited(
|
|
||||||
msg.headers().get_all(Self::name()))?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::fmt::Display for $id {
|
|
||||||
#[inline]
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match *self {
|
|
||||||
$id::Any => f.write_str("*"),
|
|
||||||
$id::Items(ref fields) => $crate::http::header::fmt_comma_delimited(
|
|
||||||
f, &fields[..])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl $crate::http::header::IntoHeaderValue for $id {
|
|
||||||
type Error = $crate::http::header::InvalidHeaderValue;
|
|
||||||
|
|
||||||
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
|
||||||
use std::fmt::Write;
|
|
||||||
let mut writer = $crate::http::header::Writer::new();
|
|
||||||
let _ = write!(&mut writer, "{}", self);
|
|
||||||
$crate::http::header::HeaderValue::from_maybe_shared(writer.take())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// optional test module
|
|
||||||
($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => {
|
|
||||||
header! {
|
|
||||||
$(#[$a])*
|
|
||||||
($id, $name) => ($item)*
|
|
||||||
}
|
|
||||||
|
|
||||||
__hyper__tm! { $id, $tm { $($tf)* }}
|
|
||||||
};
|
|
||||||
($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => {
|
|
||||||
header! {
|
|
||||||
$(#[$a])*
|
|
||||||
($id, $n) => ($item)+
|
|
||||||
}
|
|
||||||
|
|
||||||
__hyper__tm! { $id, $tm { $($tf)* }}
|
|
||||||
};
|
|
||||||
($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => {
|
|
||||||
header! {
|
|
||||||
$(#[$a])* ($id, $name) => [$item]
|
|
||||||
}
|
|
||||||
|
|
||||||
__hyper__tm! { $id, $tm { $($tf)* }}
|
|
||||||
};
|
|
||||||
($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => {
|
|
||||||
header! {
|
|
||||||
$(#[$a])*
|
|
||||||
($id, $name) => {Any / ($item)+}
|
|
||||||
}
|
|
||||||
|
|
||||||
__hyper__tm! { $id, $tm { $($tf)* }}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
mod accept_charset;
|
|
||||||
//mod accept_encoding;
|
|
||||||
mod accept;
|
|
||||||
mod accept_language;
|
|
||||||
mod allow;
|
|
||||||
mod cache_control;
|
|
||||||
mod content_disposition;
|
|
||||||
mod content_language;
|
|
||||||
mod content_range;
|
|
||||||
mod content_type;
|
|
||||||
mod date;
|
|
||||||
mod etag;
|
|
||||||
mod expires;
|
|
||||||
mod if_match;
|
|
||||||
mod if_modified_since;
|
|
||||||
mod if_none_match;
|
|
||||||
mod if_range;
|
|
||||||
mod if_unmodified_since;
|
|
||||||
mod last_modified;
|
|
||||||
@@ -1,410 +0,0 @@
|
|||||||
use std::fmt::{self, Display};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use header::parsing::from_one_raw_str;
|
|
||||||
use header::{Header, Raw};
|
|
||||||
|
|
||||||
/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1)
|
|
||||||
///
|
|
||||||
/// The "Range" header field on a GET request modifies the method
|
|
||||||
/// semantics to request transfer of only one or more sub-ranges of the
|
|
||||||
/// selected representation data, rather than the entire selected
|
|
||||||
/// representation data.
|
|
||||||
///
|
|
||||||
/// # ABNF
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// Range = byte-ranges-specifier / other-ranges-specifier
|
|
||||||
/// other-ranges-specifier = other-range-unit "=" other-range-set
|
|
||||||
/// other-range-set = 1*VCHAR
|
|
||||||
///
|
|
||||||
/// bytes-unit = "bytes"
|
|
||||||
///
|
|
||||||
/// byte-ranges-specifier = bytes-unit "=" byte-range-set
|
|
||||||
/// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec)
|
|
||||||
/// byte-range-spec = first-byte-pos "-" [last-byte-pos]
|
|
||||||
/// first-byte-pos = 1*DIGIT
|
|
||||||
/// last-byte-pos = 1*DIGIT
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Example values
|
|
||||||
///
|
|
||||||
/// * `bytes=1000-`
|
|
||||||
/// * `bytes=-2000`
|
|
||||||
/// * `bytes=0-1,30-40`
|
|
||||||
/// * `bytes=0-10,20-90,-100`
|
|
||||||
/// * `custom_unit=0-123`
|
|
||||||
/// * `custom_unit=xxx-yyy`
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use hyper::header::{Headers, Range, ByteRangeSpec};
|
|
||||||
///
|
|
||||||
/// let mut headers = Headers::new();
|
|
||||||
/// headers.set(Range::Bytes(
|
|
||||||
/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)]
|
|
||||||
/// ));
|
|
||||||
///
|
|
||||||
/// headers.clear();
|
|
||||||
/// headers.set(Range::Unregistered("letters".to_owned(), "a-f".to_owned()));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use hyper::header::{Headers, Range};
|
|
||||||
///
|
|
||||||
/// let mut headers = Headers::new();
|
|
||||||
/// headers.set(Range::bytes(1, 100));
|
|
||||||
///
|
|
||||||
/// headers.clear();
|
|
||||||
/// headers.set(Range::bytes_multi(vec![(1, 100), (200, 300)]));
|
|
||||||
/// ```
|
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
|
||||||
pub enum Range {
|
|
||||||
/// Byte range
|
|
||||||
Bytes(Vec<ByteRangeSpec>),
|
|
||||||
/// Custom range, with unit not registered at IANA
|
|
||||||
/// (`other-range-unit`: String , `other-range-set`: String)
|
|
||||||
Unregistered(String, String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Each `Range::Bytes` header can contain one or more `ByteRangeSpecs`.
|
|
||||||
/// Each `ByteRangeSpec` defines a range of bytes to fetch
|
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
|
||||||
pub enum ByteRangeSpec {
|
|
||||||
/// Get all bytes between x and y ("x-y")
|
|
||||||
FromTo(u64, u64),
|
|
||||||
/// Get all bytes starting from x ("x-")
|
|
||||||
AllFrom(u64),
|
|
||||||
/// Get last x bytes ("-x")
|
|
||||||
Last(u64),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ByteRangeSpec {
|
|
||||||
/// Given the full length of the entity, attempt to normalize the byte range
|
|
||||||
/// into an satisfiable end-inclusive (from, to) range.
|
|
||||||
///
|
|
||||||
/// The resulting range is guaranteed to be a satisfiable range within the
|
|
||||||
/// bounds of `0 <= from <= to < full_length`.
|
|
||||||
///
|
|
||||||
/// If the byte range is deemed unsatisfiable, `None` is returned.
|
|
||||||
/// An unsatisfiable range is generally cause for a server to either reject
|
|
||||||
/// the client request with a `416 Range Not Satisfiable` status code, or to
|
|
||||||
/// simply ignore the range header and serve the full entity using a `200
|
|
||||||
/// OK` status code.
|
|
||||||
///
|
|
||||||
/// This function closely follows [RFC 7233][1] section 2.1.
|
|
||||||
/// As such, it considers ranges to be satisfiable if they meet the
|
|
||||||
/// following conditions:
|
|
||||||
///
|
|
||||||
/// > If a valid byte-range-set includes at least one byte-range-spec with
|
|
||||||
/// a first-byte-pos that is less than the current length of the
|
|
||||||
/// representation, or at least one suffix-byte-range-spec with a
|
|
||||||
/// non-zero suffix-length, then the byte-range-set is satisfiable.
|
|
||||||
/// Otherwise, the byte-range-set is unsatisfiable.
|
|
||||||
///
|
|
||||||
/// The function also computes remainder ranges based on the RFC:
|
|
||||||
///
|
|
||||||
/// > If the last-byte-pos value is
|
|
||||||
/// absent, or if the value is greater than or equal to the current
|
|
||||||
/// length of the representation data, the byte range is interpreted as
|
|
||||||
/// the remainder of the representation (i.e., the server replaces the
|
|
||||||
/// value of last-byte-pos with a value that is one less than the current
|
|
||||||
/// length of the selected representation).
|
|
||||||
///
|
|
||||||
/// [1]: https://tools.ietf.org/html/rfc7233
|
|
||||||
pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> {
|
|
||||||
// If the full length is zero, there is no satisfiable end-inclusive range.
|
|
||||||
if full_length == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
match self {
|
|
||||||
&ByteRangeSpec::FromTo(from, to) => {
|
|
||||||
if from < full_length && from <= to {
|
|
||||||
Some((from, ::std::cmp::min(to, full_length - 1)))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&ByteRangeSpec::AllFrom(from) => {
|
|
||||||
if from < full_length {
|
|
||||||
Some((from, full_length - 1))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&ByteRangeSpec::Last(last) => {
|
|
||||||
if last > 0 {
|
|
||||||
// From the RFC: If the selected representation is shorter
|
|
||||||
// than the specified suffix-length,
|
|
||||||
// the entire representation is used.
|
|
||||||
if last > full_length {
|
|
||||||
Some((0, full_length - 1))
|
|
||||||
} else {
|
|
||||||
Some((full_length - last, full_length - 1))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Range {
|
|
||||||
/// Get the most common byte range header ("bytes=from-to")
|
|
||||||
pub fn bytes(from: u64, to: u64) -> Range {
|
|
||||||
Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get byte range header with multiple subranges
|
|
||||||
/// ("bytes=from1-to1,from2-to2,fromX-toX")
|
|
||||||
pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range {
|
|
||||||
Range::Bytes(
|
|
||||||
ranges
|
|
||||||
.iter()
|
|
||||||
.map(|r| ByteRangeSpec::FromTo(r.0, r.1))
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ByteRangeSpec {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to),
|
|
||||||
ByteRangeSpec::Last(pos) => write!(f, "-{}", pos),
|
|
||||||
ByteRangeSpec::AllFrom(pos) => write!(f, "{}-", pos),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Range {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
Range::Bytes(ref ranges) => {
|
|
||||||
write!(f, "bytes=")?;
|
|
||||||
|
|
||||||
for (i, range) in ranges.iter().enumerate() {
|
|
||||||
if i != 0 {
|
|
||||||
f.write_str(",")?;
|
|
||||||
}
|
|
||||||
Display::fmt(range, f)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Range::Unregistered(ref unit, ref range_str) => {
|
|
||||||
write!(f, "{}={}", unit, range_str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Range {
|
|
||||||
type Err = ::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> ::Result<Range> {
|
|
||||||
let mut iter = s.splitn(2, '=');
|
|
||||||
|
|
||||||
match (iter.next(), iter.next()) {
|
|
||||||
(Some("bytes"), Some(ranges)) => {
|
|
||||||
let ranges = from_comma_delimited(ranges);
|
|
||||||
if ranges.is_empty() {
|
|
||||||
return Err(::Error::Header);
|
|
||||||
}
|
|
||||||
Ok(Range::Bytes(ranges))
|
|
||||||
}
|
|
||||||
(Some(unit), Some(range_str)) if unit != "" && range_str != "" => {
|
|
||||||
Ok(Range::Unregistered(unit.to_owned(), range_str.to_owned()))
|
|
||||||
}
|
|
||||||
_ => Err(::Error::Header),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ByteRangeSpec {
|
|
||||||
type Err = ::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> ::Result<ByteRangeSpec> {
|
|
||||||
let mut parts = s.splitn(2, '-');
|
|
||||||
|
|
||||||
match (parts.next(), parts.next()) {
|
|
||||||
(Some(""), Some(end)) => end
|
|
||||||
.parse()
|
|
||||||
.or(Err(::Error::Header))
|
|
||||||
.map(ByteRangeSpec::Last),
|
|
||||||
(Some(start), Some("")) => start
|
|
||||||
.parse()
|
|
||||||
.or(Err(::Error::Header))
|
|
||||||
.map(ByteRangeSpec::AllFrom),
|
|
||||||
(Some(start), Some(end)) => match (start.parse(), end.parse()) {
|
|
||||||
(Ok(start), Ok(end)) if start <= end => {
|
|
||||||
Ok(ByteRangeSpec::FromTo(start, end))
|
|
||||||
}
|
|
||||||
_ => Err(::Error::Header),
|
|
||||||
},
|
|
||||||
_ => Err(::Error::Header),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_comma_delimited<T: FromStr>(s: &str) -> Vec<T> {
|
|
||||||
s.split(',')
|
|
||||||
.filter_map(|x| match x.trim() {
|
|
||||||
"" => None,
|
|
||||||
y => Some(y),
|
|
||||||
})
|
|
||||||
.filter_map(|x| x.parse().ok())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Header for Range {
|
|
||||||
fn header_name() -> &'static str {
|
|
||||||
static NAME: &'static str = "Range";
|
|
||||||
NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_header(raw: &Raw) -> ::Result<Range> {
|
|
||||||
from_one_raw_str(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result {
|
|
||||||
f.fmt_line(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_bytes_range_valid() {
|
|
||||||
let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap();
|
|
||||||
let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap();
|
|
||||||
let r3 = Range::bytes(1, 100);
|
|
||||||
assert_eq!(r, r2);
|
|
||||||
assert_eq!(r2, r3);
|
|
||||||
|
|
||||||
let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap();
|
|
||||||
let r2: Range =
|
|
||||||
Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap();
|
|
||||||
let r3 = Range::Bytes(vec![
|
|
||||||
ByteRangeSpec::FromTo(1, 100),
|
|
||||||
ByteRangeSpec::AllFrom(200),
|
|
||||||
]);
|
|
||||||
assert_eq!(r, r2);
|
|
||||||
assert_eq!(r2, r3);
|
|
||||||
|
|
||||||
let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap();
|
|
||||||
let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap();
|
|
||||||
let r3 = Range::Bytes(vec![
|
|
||||||
ByteRangeSpec::FromTo(1, 100),
|
|
||||||
ByteRangeSpec::Last(100),
|
|
||||||
]);
|
|
||||||
assert_eq!(r, r2);
|
|
||||||
assert_eq!(r2, r3);
|
|
||||||
|
|
||||||
let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap();
|
|
||||||
let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned());
|
|
||||||
assert_eq!(r, r2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_unregistered_range_valid() {
|
|
||||||
let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap();
|
|
||||||
let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned());
|
|
||||||
assert_eq!(r, r2);
|
|
||||||
|
|
||||||
let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap();
|
|
||||||
let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned());
|
|
||||||
assert_eq!(r, r2);
|
|
||||||
|
|
||||||
let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap();
|
|
||||||
let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned());
|
|
||||||
assert_eq!(r, r2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_invalid() {
|
|
||||||
let r: ::Result<Range> = Header::parse_header(&"bytes=1-a,-".into());
|
|
||||||
assert_eq!(r.ok(), None);
|
|
||||||
|
|
||||||
let r: ::Result<Range> = Header::parse_header(&"bytes=1-2-3".into());
|
|
||||||
assert_eq!(r.ok(), None);
|
|
||||||
|
|
||||||
let r: ::Result<Range> = Header::parse_header(&"abc".into());
|
|
||||||
assert_eq!(r.ok(), None);
|
|
||||||
|
|
||||||
let r: ::Result<Range> = Header::parse_header(&"bytes=1-100=".into());
|
|
||||||
assert_eq!(r.ok(), None);
|
|
||||||
|
|
||||||
let r: ::Result<Range> = Header::parse_header(&"bytes=".into());
|
|
||||||
assert_eq!(r.ok(), None);
|
|
||||||
|
|
||||||
let r: ::Result<Range> = Header::parse_header(&"custom=".into());
|
|
||||||
assert_eq!(r.ok(), None);
|
|
||||||
|
|
||||||
let r: ::Result<Range> = Header::parse_header(&"=1-100".into());
|
|
||||||
assert_eq!(r.ok(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fmt() {
|
|
||||||
use header::Headers;
|
|
||||||
|
|
||||||
let mut headers = Headers::new();
|
|
||||||
|
|
||||||
headers.set(Range::Bytes(vec![
|
|
||||||
ByteRangeSpec::FromTo(0, 1000),
|
|
||||||
ByteRangeSpec::AllFrom(2000),
|
|
||||||
]));
|
|
||||||
assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n");
|
|
||||||
|
|
||||||
headers.clear();
|
|
||||||
headers.set(Range::Bytes(vec![]));
|
|
||||||
|
|
||||||
assert_eq!(&headers.to_string(), "Range: bytes=\r\n");
|
|
||||||
|
|
||||||
headers.clear();
|
|
||||||
headers.set(Range::Unregistered("custom".to_owned(), "1-xxx".to_owned()));
|
|
||||||
|
|
||||||
assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_byte_range_spec_to_satisfiable_range() {
|
|
||||||
assert_eq!(
|
|
||||||
Some((0, 0)),
|
|
||||||
ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Some((1, 2)),
|
|
||||||
ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Some((1, 2)),
|
|
||||||
ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3)
|
|
||||||
);
|
|
||||||
assert_eq!(None, ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3));
|
|
||||||
assert_eq!(None, ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3));
|
|
||||||
assert_eq!(None, ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Some((0, 2)),
|
|
||||||
ByteRangeSpec::AllFrom(0).to_satisfiable_range(3)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Some((2, 2)),
|
|
||||||
ByteRangeSpec::AllFrom(2).to_satisfiable_range(3)
|
|
||||||
);
|
|
||||||
assert_eq!(None, ByteRangeSpec::AllFrom(3).to_satisfiable_range(3));
|
|
||||||
assert_eq!(None, ByteRangeSpec::AllFrom(5).to_satisfiable_range(3));
|
|
||||||
assert_eq!(None, ByteRangeSpec::AllFrom(0).to_satisfiable_range(0));
|
|
||||||
|
|
||||||
assert_eq!(Some((1, 2)), ByteRangeSpec::Last(2).to_satisfiable_range(3));
|
|
||||||
assert_eq!(Some((2, 2)), ByteRangeSpec::Last(1).to_satisfiable_range(3));
|
|
||||||
assert_eq!(Some((0, 2)), ByteRangeSpec::Last(5).to_satisfiable_range(3));
|
|
||||||
assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3));
|
|
||||||
assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
122
actix-http/src/header/into_pair.rs
Normal file
122
actix-http/src/header/into_pair.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
//! [`IntoHeaderPair`] trait and implementations.
|
||||||
|
|
||||||
|
use std::convert::TryFrom as _;
|
||||||
|
|
||||||
|
use http::{
|
||||||
|
header::{HeaderName, InvalidHeaderName, InvalidHeaderValue},
|
||||||
|
Error as HttpError, HeaderValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Header, IntoHeaderValue};
|
||||||
|
|
||||||
|
/// An interface for types that can be converted into a [`HeaderName`]/[`HeaderValue`] pair for
|
||||||
|
/// insertion into a [`HeaderMap`].
|
||||||
|
///
|
||||||
|
/// [`HeaderMap`]: crate::http::HeaderMap
|
||||||
|
pub trait IntoHeaderPair: Sized {
|
||||||
|
type Error: Into<HttpError>;
|
||||||
|
|
||||||
|
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InvalidHeaderPart {
|
||||||
|
Name(InvalidHeaderName),
|
||||||
|
Value(InvalidHeaderValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<InvalidHeaderPart> for HttpError {
|
||||||
|
fn from(part_err: InvalidHeaderPart) -> Self {
|
||||||
|
match part_err {
|
||||||
|
InvalidHeaderPart::Name(err) => err.into(),
|
||||||
|
InvalidHeaderPart::Value(err) => err.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> IntoHeaderPair for (HeaderName, V)
|
||||||
|
where
|
||||||
|
V: IntoHeaderValue,
|
||||||
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
|
{
|
||||||
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
|
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
|
let (name, value) = self;
|
||||||
|
let value = value
|
||||||
|
.try_into_value()
|
||||||
|
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
|
||||||
|
Ok((name, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> IntoHeaderPair for (&HeaderName, V)
|
||||||
|
where
|
||||||
|
V: IntoHeaderValue,
|
||||||
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
|
{
|
||||||
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
|
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
|
let (name, value) = self;
|
||||||
|
let value = value
|
||||||
|
.try_into_value()
|
||||||
|
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
|
||||||
|
Ok((name.clone(), value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> IntoHeaderPair for (&[u8], V)
|
||||||
|
where
|
||||||
|
V: IntoHeaderValue,
|
||||||
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
|
{
|
||||||
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
|
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
|
let (name, value) = self;
|
||||||
|
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
|
||||||
|
let value = value
|
||||||
|
.try_into_value()
|
||||||
|
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
|
||||||
|
Ok((name, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> IntoHeaderPair for (&str, V)
|
||||||
|
where
|
||||||
|
V: IntoHeaderValue,
|
||||||
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
|
{
|
||||||
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
|
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
|
let (name, value) = self;
|
||||||
|
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
|
||||||
|
let value = value
|
||||||
|
.try_into_value()
|
||||||
|
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
|
||||||
|
Ok((name, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> IntoHeaderPair for (String, V)
|
||||||
|
where
|
||||||
|
V: IntoHeaderValue,
|
||||||
|
V::Error: Into<InvalidHeaderValue>,
|
||||||
|
{
|
||||||
|
type Error = InvalidHeaderPart;
|
||||||
|
|
||||||
|
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
|
let (name, value) = self;
|
||||||
|
(name.as_str(), value).try_into_header_pair()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Header> IntoHeaderPair for T {
|
||||||
|
type Error = <T as IntoHeaderValue>::Error;
|
||||||
|
|
||||||
|
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
|
||||||
|
Ok((T::name(), self.try_into_value()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
133
actix-http/src/header/into_value.rs
Normal file
133
actix-http/src/header/into_value.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
//! [`IntoHeaderValue`] trait and implementations.
|
||||||
|
|
||||||
|
use std::convert::TryFrom as _;
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
|
||||||
|
use mime::Mime;
|
||||||
|
|
||||||
|
/// An interface for types that can be converted into a [`HeaderValue`].
|
||||||
|
pub trait IntoHeaderValue: Sized {
|
||||||
|
/// The type returned in the event of a conversion error.
|
||||||
|
type Error: Into<HttpError>;
|
||||||
|
|
||||||
|
/// Try to convert value to a HeaderValue.
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for HeaderValue {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for &HeaderValue {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
Ok(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for &str {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
self.parse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for &[u8] {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::from_bytes(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for Bytes {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::from_maybe_shared(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for Vec<u8> {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::try_from(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for String {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::try_from(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for usize {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::try_from(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for i64 {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::try_from(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for u64 {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::try_from(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for i32 {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::try_from(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for u32 {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::try_from(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoHeaderValue for Mime {
|
||||||
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||||
|
HeaderValue::from_str(self.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user