mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-03 09:36:36 +02:00
Compare commits
246 Commits
codegen-v0
...
web-v4.0.0
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
c09186a2c0 | |||
d3c476b8c2 | |||
dc23559f23 | |||
6d710629af | |||
85753130d9 | |||
00ba8d5549 | |||
51e9e1500b | |||
a03dbe2dcf | |||
57a3722146 | |||
57da1d3c0f | |||
68117543ea | |||
4f5971d79e | |||
93161df141 | |||
e567873326 | |||
7d632d0b7b | |||
36aee18c64 | |||
007a145988 | |||
2d4a174420 | |||
21f6c9d7a5 | |||
e1683313ec | |||
32de9f8840 | |||
1f202d40e4 | |||
ad608aa64e | |||
a1b00b2cd0 | |||
3beb4cf2da | |||
522c9a5ea6 | |||
102bb8f9ab | |||
20b46cdaf9 | |||
2a2a20c3e7 | |||
093d3a6c59 | |||
8c9ea43e23 | |||
f9fcf56d5c | |||
cbda928a33 | |||
1032f04ded | |||
b373e1370d | |||
404b5a7709 | |||
ecf08d5156 | |||
87655b3028 | |||
3a192400a6 | |||
2a7f2c1d59 | |||
05f104c240 | |||
4dccd092f3 | |||
95ccf1c9bc | |||
6cbf27508a | |||
79de04d862 | |||
a4dbaa8ed1 | |||
c7b4c6edfa | |||
2a5215c1d6 | |||
97f615c245 | |||
1a361273e7 | |||
d7ce648445 | |||
fabc68659b | |||
542db82282 | |||
ae63eb8bb2 | |||
7a3776b770 | |||
ff79c33fd4 | |||
b75a9b7a20 | |||
d0c6ca7671 | |||
24d525d978 | |||
1f70ef155d | |||
7981e0068a | |||
32d59ca904 | |||
ea8bf36104 | |||
0b5b463cfa | |||
fe6ad816cc | |||
e72b787ba7 | |||
efc317d3b0 | |||
31057becca | |||
f1a9b45437 | |||
5af46775b8 | |||
70f4747a23 | |||
2f11ef089b | |||
4100c50c70 | |||
a929209967 | |||
49e945c88f | |||
9b42333fac | |||
e5b86d189c | |||
4bfd5c2781 | |||
9b6a089b36 | |||
ceac97bb8d | |||
61b65aa64a | |||
5468c3c410 | |||
b6385c2b4e | |||
5135c1e3a0 | |||
22b451cf2d | |||
42f51eb962 | |||
156c97cef2 | |||
798d744eef | |||
4cb833616a | |||
9963a5ef54 | |||
4519db36b2 | |||
7030bf5fe8 | |||
20078fe603 | |||
06e5042b94 | |||
41e7cec72f | |||
d45a1aa6b6 | |||
98243db9f1 | |||
f92742bdac | |||
e563025b16 | |||
cfd5b381f1 | |||
2f84914146 | |||
d765e9099d | |||
34b23f31c9 | |||
26c1a901d9 | |||
c2c71cc626 | |||
aa11231ee5 | |||
b5812b15f0 | |||
b4e02fe29a | |||
37c76a39ab | |||
60e7e52276 | |||
c53e9468bc | |||
162121bf8d | |||
f7bcad9567 | |||
f9e3f78e45 | |||
1596893ef7 | |||
2a2474ca09 | |||
509b2e6eec | |||
d707704556 | |||
a429ee6646 | |||
7f8073233a | |||
4b4c9d1b93 | |||
3fde3be3d8 | |||
f861508789 | |||
a4546f02d2 | |||
64a2c13cdf | |||
bf53fe5a22 | |||
cf5138e740 | |||
121075c1ef | |||
22089aff87 | |||
7787638f26 | |||
2f6e9738c4 | |||
e39d166a17 | |||
059d1671d7 | |||
3a27580ebe | |||
9d0534999d | |||
c54d73e0bb | |||
9a9d4b182e | |||
4e321595bc | |||
01cbef700f | |||
8497b5f490 | |||
75d86a6beb | |||
3892a95c11 | |||
5802eb797f | |||
ff2ca0f420 | |||
59ad1738e9 | |||
aa2bd6fbfb | |||
5aad8e24c7 | |||
6e97bc09f8 | |||
160995b8d4 | |||
187646b2f9 | |||
46627be36f | |||
a78380739e | |||
cf1c8abe62 | |||
92b5bcd13f | |||
701bdacfa2 | |||
6dc47c4093 | |||
0ec335a39c | |||
f8d5ad6b53 | |||
43c362779d | |||
971ba3eee1 | |||
2fd96c03e5 | |||
ad7c6d2633 | |||
3362a3d61b | |||
769ea6bd5b | |||
1382094c15 | |||
78594a72bd | |||
327e472e44 | |||
e10eb648d9 | |||
a2662b928b | |||
84583799be | |||
08f9a34075 | |||
056803d534 | |||
deab634247 | |||
0b641a2db2 | |||
f2d641b772 | |||
23c8191cca | |||
487f90be5b | |||
fa28175a74 | |||
a70e599ff5 | |||
c11052f826 | |||
73ec01e83b | |||
a7c8533291 | |||
eb0eda69c6 | |||
dc74db1f2f | |||
0ba73fc44c | |||
9af07d66ae | |||
e72ee28232 | |||
4f9a1ac3b7 | |||
6c5c4ea230 | |||
a79450482c | |||
5286b8aed7 | |||
621ebec01a | |||
5e5c8b1c83 | |||
322e7c15d1 | |||
7aa757ad5a | |||
482f74e409 | |||
19967c41cc | |||
8a106c07b4 | |||
11a9fdad95 | |||
75a34dc8bc |
@ -1,41 +0,0 @@
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: actix-web
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: stable
|
||||
# Nightly channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: nightly
|
||||
|
||||
# Install Rust and Cargo
|
||||
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
|
||||
install:
|
||||
- ps: >-
|
||||
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw64\bin'
|
||||
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\MinGW\bin'
|
||||
}
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs
|
||||
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
|
||||
# 'cargo test' takes care of building for us, so disable Appveyor's build stage.
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo clean
|
||||
- cargo test --no-default-features --features="flate2-rust"
|
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: bug report
|
||||
about: create a bug report
|
||||
name: Bug Report
|
||||
about: Create a bug report.
|
||||
---
|
||||
|
||||
Your issue may already be reported!
|
||||
|
15
.github/ISSUE_TEMPLATE/config.yml
vendored
15
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,15 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Gitter channel (actix-web)
|
||||
- name: GitHub Discussions
|
||||
url: https://github.com/actix/actix-web/discussions
|
||||
about: Actix Web Q&A
|
||||
- name: Gitter chat (actix-web)
|
||||
url: https://gitter.im/actix/actix-web
|
||||
about: Please ask and answer questions about the actix-web here.
|
||||
- name: Gitter channel (actix)
|
||||
about: Actix Web Q&A
|
||||
- name: Gitter chat (actix)
|
||||
url: https://gitter.im/actix/actix
|
||||
about: Please ask and answer questions about the actix here.
|
||||
about: Actix (actor framework) Q&A
|
||||
- name: Actix Discord
|
||||
url: https://discord.gg/NWpN5mmg3x
|
||||
about: Actix developer discussion and community chat
|
||||
|
||||
|
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
<!-- Thanks for considering contributing actix! -->
|
||||
<!-- Please fill out the following to get your PR reviewed quicker. -->
|
||||
|
||||
## PR Type
|
||||
<!-- What kind of change does this PR make? -->
|
||||
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
|
||||
PR_TYPE
|
||||
|
||||
|
||||
## PR Checklist
|
||||
<!-- Check your PR fulfills the following items. ->>
|
||||
<!-- For draft PRs check the boxes as you complete them. -->
|
||||
|
||||
- [ ] Tests for the changes have been added / updated.
|
||||
- [ ] Documentation comments have been added / updated.
|
||||
- [ ] A changelog entry has been made for the appropriate packages.
|
||||
- [ ] Format code with the latest stable rustfmt.
|
||||
- [ ] (Team) Label with affected crates and semver status.
|
||||
|
||||
|
||||
## Overview
|
||||
<!-- Describe the current and new behavior. -->
|
||||
<!-- Emphasize any breaking changes. -->
|
||||
|
||||
|
||||
<!-- If this PR fixes or closes an issue, reference it here. -->
|
||||
<!-- Closes #000 -->
|
11
.github/workflows/bench.yml
vendored
11
.github/workflows/bench.yml
vendored
@ -1,13 +1,18 @@
|
||||
name: Benchmark (Linux)
|
||||
name: Benchmark
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
check_benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
39
.github/workflows/clippy-fmt.yml
vendored
Normal file
39
.github/workflows/clippy-fmt.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
- name: Check with rustfmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Check with Clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --workspace --tests --all-features
|
29
.github/workflows/linux.yml
vendored
29
.github/workflows/linux.yml
vendored
@ -1,6 +1,11 @@
|
||||
name: CI (Linux)
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
@ -8,7 +13,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- 1.40.0 # MSRV
|
||||
- 1.46.0 # MSRV
|
||||
- stable
|
||||
- nightly
|
||||
|
||||
@ -16,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
@ -25,6 +30,13 @@ jobs:
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.0.1
|
||||
|
||||
- name: check build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@ -53,12 +65,17 @@ jobs:
|
||||
args: --package=awc --no-default-features --features=rustls -- --nocapture
|
||||
|
||||
- name: Generate coverage file
|
||||
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
|
||||
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
cargo install cargo-tarpaulin
|
||||
cargo install cargo-tarpaulin --vers "^0.13"
|
||||
cargo tarpaulin --out Xml
|
||||
- name: Upload to Codecov
|
||||
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
|
||||
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: cobertura.xml
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
21
.github/workflows/macos.yml
vendored
21
.github/workflows/macos.yml
vendored
@ -1,6 +1,11 @@
|
||||
name: CI (macOS)
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
@ -15,7 +20,7 @@ jobs:
|
||||
runs-on: macOS-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
@ -24,6 +29,13 @@ jobs:
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.0.1
|
||||
|
||||
- name: check build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@ -37,3 +49,8 @@ jobs:
|
||||
args: --all --all-features --no-fail-fast -- --nocapture
|
||||
--skip=test_h2_content_length
|
||||
--skip=test_reading_deflate_encoding_large_random_rustls
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
16
.github/workflows/upload-doc.yml
vendored
16
.github/workflows/upload-doc.yml
vendored
@ -11,12 +11,12 @@ jobs:
|
||||
if: github.repository == 'actix/actix-web'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
toolchain: nightly-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
@ -24,12 +24,14 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --no-deps --workspace --all-features
|
||||
args: --workspace --all-features --no-deps
|
||||
|
||||
- name: Tweak HTML
|
||||
run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html
|
||||
|
||||
- name: Upload documentation
|
||||
run: |
|
||||
git clone https://github.com/davisp/ghp-import.git
|
||||
./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://${{ secrets.GITHUB_TOKEN }}@github.com/"${{ github.repository }}.git" target/doc
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: target/doc
|
||||
|
21
.github/workflows/windows.yml
vendored
21
.github/workflows/windows.yml
vendored
@ -1,6 +1,11 @@
|
||||
name: CI (Windows)
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
VCPKGRS_DYNAMIC: 1
|
||||
@ -18,7 +23,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
@ -36,6 +41,13 @@ jobs:
|
||||
Get-ChildItem C:\vcpkg\installed\x64-windows\bin
|
||||
Get-ChildItem C:\vcpkg\installed\x64-windows\lib
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.0.1
|
||||
|
||||
- name: check build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@ -57,3 +69,8 @@ jobs:
|
||||
--skip=test_connection_force_close
|
||||
--skip=test_connection_server_close
|
||||
--skip=test_connection_wait_queue_force_close
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -9,6 +9,10 @@ guide/build/
|
||||
*.pid
|
||||
*.sock
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Configuration directory generated by CLion
|
||||
.idea
|
||||
|
229
CHANGES.md
229
CHANGES.md
@ -1,19 +1,232 @@
|
||||
# Changes
|
||||
|
||||
## [3.0.0-alpha.3] - 2020-05-21
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 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
|
||||
### Added
|
||||
* `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and
|
||||
`Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865]
|
||||
|
||||
### Changed
|
||||
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
|
||||
* Bumped `rand` to `0.8`.
|
||||
* Update `rust-tls` to `0.19`. [#1813]
|
||||
* Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852]
|
||||
* The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration
|
||||
guide for implications. [#1875]
|
||||
* Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875]
|
||||
* MSRV is now 1.46.0.
|
||||
|
||||
### Fixed
|
||||
* Added the underlying parse error to `test::read_body_json`'s panic message. [#1812]
|
||||
|
||||
### Removed
|
||||
* Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now
|
||||
exposed directly by the `middleware` module.
|
||||
* 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
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
[#1852]: https://github.com/actix/actix-web/pull/1852
|
||||
[#1865]: https://github.com/actix/actix-web/pull/1865
|
||||
[#1875]: https://github.com/actix/actix-web/pull/1875
|
||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||
|
||||
## 3.3.2 - 2020-12-01
|
||||
### Fixed
|
||||
* Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762]
|
||||
* Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798]
|
||||
* Increase minimum `socket2` version. [#1803]
|
||||
|
||||
[#1762]: https://github.com/actix/actix-web/pull/1762
|
||||
[#1798]: https://github.com/actix/actix-web/pull/1798
|
||||
[#1803]: https://github.com/actix/actix-web/pull/1803
|
||||
|
||||
|
||||
## 3.3.1 - 2020-11-29
|
||||
* Ensure `actix-http` dependency uses same `serde_urlencoded`.
|
||||
|
||||
|
||||
## 3.3.0 - 2020-11-25
|
||||
### Added
|
||||
* Add `Either<A, B>` extractor helper. [#1788]
|
||||
|
||||
### Changed
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1788]: https://github.com/actix/actix-web/pull/1788
|
||||
|
||||
|
||||
## 3.2.0 - 2020-10-30
|
||||
### Added
|
||||
* Implement `exclude_regex` for Logger middleware. [#1723]
|
||||
* Add request-local data extractor `web::ReqData`. [#1748]
|
||||
* Add ability to register closure for request middleware logging. [#1749]
|
||||
* Add `app_data` to `ServiceConfig`. [#1757]
|
||||
* Expose `on_connect` for access to the connection stream before request is handled. [#1754]
|
||||
|
||||
### Changed
|
||||
* Updated actix-web-codegen dependency for access to new `#[route(...)]` multi-method macro.
|
||||
* Print non-configured `Data<T>` type when attempting extraction. [#1743]
|
||||
* Re-export bytes::Buf{Mut} in web module. [#1750]
|
||||
* Upgrade `pin-project` to `1.0`.
|
||||
|
||||
[#1723]: https://github.com/actix/actix-web/pull/1723
|
||||
[#1743]: https://github.com/actix/actix-web/pull/1743
|
||||
[#1748]: https://github.com/actix/actix-web/pull/1748
|
||||
[#1750]: https://github.com/actix/actix-web/pull/1750
|
||||
[#1754]: https://github.com/actix/actix-web/pull/1754
|
||||
[#1749]: https://github.com/actix/actix-web/pull/1749
|
||||
|
||||
|
||||
## 3.1.0 - 2020-09-29
|
||||
### Changed
|
||||
* Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath`
|
||||
to retain any trailing slashes. [#1695]
|
||||
* Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc<dyn Trait>`
|
||||
via `web::Data::from` [#1710]
|
||||
|
||||
### Fixed
|
||||
* `ResourceMap` debug printing is no longer infinitely recursive. [#1708]
|
||||
|
||||
[#1695]: https://github.com/actix/actix-web/pull/1695
|
||||
[#1708]: https://github.com/actix/actix-web/pull/1708
|
||||
[#1710]: https://github.com/actix/actix-web/pull/1710
|
||||
|
||||
|
||||
## 3.0.2 - 2020-09-15
|
||||
### Fixed
|
||||
* `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678]
|
||||
|
||||
[#1678]: https://github.com/actix/actix-web/pull/1678
|
||||
|
||||
|
||||
## 3.0.1 - 2020-09-13
|
||||
### Changed
|
||||
* `middleware::normalize::TrailingSlash` enum is now accessible. [#1673]
|
||||
|
||||
[#1673]: https://github.com/actix/actix-web/pull/1673
|
||||
|
||||
|
||||
## 3.0.0 - 2020-09-11
|
||||
* No significant changes from `3.0.0-beta.4`.
|
||||
|
||||
|
||||
## 3.0.0-beta.4 - 2020-09-09
|
||||
### Added
|
||||
* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing
|
||||
slash, or as the new addition, always trimming trailing slashes. [#1639]
|
||||
|
||||
### Changed
|
||||
* Update actix-codec and actix-utils dependencies. [#1634]
|
||||
* `FormConfig` and `JsonConfig` configurations are now also considered when set
|
||||
using `App::data`. [#1641]
|
||||
* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655]
|
||||
* `HttpServer::maxconnrate` is renamed to the more expressive
|
||||
`HttpServer::max_connection_rate`. [#1655]
|
||||
|
||||
[#1639]: https://github.com/actix/actix-web/pull/1639
|
||||
[#1641]: https://github.com/actix/actix-web/pull/1641
|
||||
[#1634]: https://github.com/actix/actix-web/pull/1634
|
||||
[#1655]: https://github.com/actix/actix-web/pull/1655
|
||||
|
||||
## 3.0.0-beta.3 - 2020-08-17
|
||||
### Changed
|
||||
* Update `rustls` to 0.18
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2020-08-17
|
||||
### Changed
|
||||
* `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set
|
||||
using `App::data`. [#1610]
|
||||
* `web::Path` now has a public representation: `web::Path(pub T)` that enables
|
||||
destructuring. [#1594]
|
||||
* `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to
|
||||
access `HttpRequest` which already allows this. [#1618]
|
||||
* Re-export all error types from `awc`. [#1621]
|
||||
* MSRV is now 1.42.0.
|
||||
|
||||
### Fixed
|
||||
* Memory leak of app data in pooled requests. [#1609]
|
||||
|
||||
[#1594]: https://github.com/actix/actix-web/pull/1594
|
||||
[#1609]: https://github.com/actix/actix-web/pull/1609
|
||||
[#1610]: https://github.com/actix/actix-web/pull/1610
|
||||
[#1618]: https://github.com/actix/actix-web/pull/1618
|
||||
[#1621]: https://github.com/actix/actix-web/pull/1621
|
||||
|
||||
|
||||
## 3.0.0-beta.1 - 2020-07-13
|
||||
### Added
|
||||
* Re-export `actix_rt::main` as `actix_web::main`.
|
||||
* `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched
|
||||
resource pattern.
|
||||
* `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name.
|
||||
|
||||
### Changed
|
||||
* Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550]
|
||||
* Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency.
|
||||
* MSRV is now 1.41.1
|
||||
|
||||
### Fixed
|
||||
* `NormalizePath` improved consistency when path needs slashes added _and_ removed.
|
||||
|
||||
|
||||
## 3.0.0-alpha.3 - 2020-05-21
|
||||
### Added
|
||||
* Add option to create `Data<T>` from `Arc<T>` [#1509]
|
||||
|
||||
### Changed
|
||||
|
||||
* Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486]
|
||||
|
||||
* Fix audit issue logging by default peer address [#1485]
|
||||
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
|
||||
* Replace deprecated `net2` crate with `socket2`
|
||||
|
||||
[#1485]: https://github.com/actix/actix-web/pull/1485
|
||||
@ -96,7 +309,7 @@
|
||||
|
||||
### Deleted
|
||||
|
||||
* Delete HttpServer::run(), it is not useful witht async/await
|
||||
* Delete HttpServer::run(), it is not useful with async/await
|
||||
|
||||
## [2.0.0-alpha.3] - 2019-12-07
|
||||
|
||||
@ -141,7 +354,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
* Make UrlEncodedError::Overflow more informativve
|
||||
* Make UrlEncodedError::Overflow more informative
|
||||
|
||||
* Use actix-testing for testing utils
|
||||
|
||||
@ -159,7 +372,7 @@
|
||||
|
||||
* Re-implement Host predicate (#989)
|
||||
|
||||
* Form immplements Responder, returning a `application/x-www-form-urlencoded` response
|
||||
* Form implements Responder, returning a `application/x-www-form-urlencoded` response
|
||||
|
||||
* Add `into_inner` to `Data`
|
||||
|
||||
|
@ -34,10 +34,13 @@ This Code of Conduct applies both within project spaces and in public spaces whe
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fafhrd91@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at robjtede@icloud.com ([@robjtede]) or huyuumi@neet.club ([@JohnTitor]). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
[@robjtede]: https://github.com/robjtede
|
||||
[@JohnTitor]: https://github.com/JohnTitor
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
92
Cargo.toml
92
Cargo.toml
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "3.0.0-alpha.3"
|
||||
version = "4.0.0-beta.3"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
|
||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
homepage = "https://actix.rs"
|
||||
@ -11,7 +11,7 @@ documentation = "https://docs.rs/actix-web/"
|
||||
categories = ["network-programming", "asynchronous",
|
||||
"web-programming::http-server",
|
||||
"web-programming::websocket"]
|
||||
license = "MIT/Apache-2.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
@ -31,11 +31,10 @@ members = [
|
||||
"awc",
|
||||
"actix-http",
|
||||
"actix-files",
|
||||
"actix-framed",
|
||||
"actix-multipart",
|
||||
"actix-web-actors",
|
||||
"actix-web-codegen",
|
||||
"test-server",
|
||||
"actix-http-test",
|
||||
]
|
||||
|
||||
[features]
|
||||
@ -44,14 +43,14 @@ default = ["compress"]
|
||||
# content-encoding support
|
||||
compress = ["actix-http/compress", "awc/compress"]
|
||||
|
||||
# sessions feature, session require "ring" crate and c compiler
|
||||
# sessions feature
|
||||
secure-cookies = ["actix-http/secure-cookies"]
|
||||
|
||||
# openssl
|
||||
openssl = ["actix-tls/openssl", "awc/openssl", "open-ssl"]
|
||||
openssl = ["tls_openssl", "actix-tls/accept", "actix-tls/openssl", "awc/openssl"]
|
||||
|
||||
# rustls
|
||||
rustls = ["actix-tls/rustls", "awc/rustls", "rust-tls"]
|
||||
rustls = ["tls_rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"]
|
||||
|
||||
[[example]]
|
||||
name = "basic"
|
||||
@ -65,47 +64,53 @@ required-features = ["compress"]
|
||||
name = "test_server"
|
||||
required-features = ["compress"]
|
||||
|
||||
[[example]]
|
||||
name = "on_connect"
|
||||
required-features = []
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
required-features = ["rustls"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.2.0"
|
||||
actix-service = "1.0.2"
|
||||
actix-utils = "1.0.6"
|
||||
actix-router = "0.2.4"
|
||||
actix-rt = "1.0.0"
|
||||
actix-server = "1.0.0"
|
||||
actix-testing = "1.0.0"
|
||||
actix-macros = "0.1.0"
|
||||
actix-threadpool = "0.3.1"
|
||||
actix-tls = "2.0.0-alpha.1"
|
||||
actix-codec = "0.4.0-beta.1"
|
||||
actix-macros = "0.2.0"
|
||||
actix-router = "0.2.7"
|
||||
actix-rt = "2"
|
||||
actix-server = "2.0.0-beta.3"
|
||||
actix-service = "2.0.0-beta.4"
|
||||
actix-utils = "3.0.0-beta.2"
|
||||
actix-tls = { version = "3.0.0-beta.3", default-features = false, optional = true }
|
||||
|
||||
actix-web-codegen = "0.2.2"
|
||||
actix-http = "2.0.0-alpha.4"
|
||||
awc = { version = "2.0.0-alpha.2", default-features = false }
|
||||
actix-web-codegen = "0.5.0-beta.1"
|
||||
actix-http = "3.0.0-beta.2"
|
||||
awc = { version = "3.0.0-beta.2", default-features = false }
|
||||
|
||||
bytes = "0.5.3"
|
||||
derive_more = "0.99.2"
|
||||
ahash = "0.7"
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
either = "1.5.3"
|
||||
encoding_rs = "0.8"
|
||||
futures-channel = { version = "0.3.5", default-features = false }
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
fxhash = "0.2.1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
socket2 = "0.3"
|
||||
pin-project = "0.4.6"
|
||||
regex = "1.3"
|
||||
serde = { version = "1.0", features=["derive"] }
|
||||
socket2 = "0.3.16"
|
||||
pin-project = "1.0.0"
|
||||
regex = "1.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.6.1"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
serde_urlencoded = "0.7"
|
||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
||||
url = "2.1"
|
||||
open-ssl = { version="0.10", package = "openssl", optional = true }
|
||||
rust-tls = { version = "0.17.0", package = "rustls", optional = true }
|
||||
tinyvec = { version = "0.3", features = ["alloc"] }
|
||||
tls_openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||
tls_rustls = { package = "rustls", version = "0.19.0", optional = true }
|
||||
smallvec = "1.6"
|
||||
|
||||
[dev-dependencies]
|
||||
actix = "0.10.0-alpha.1"
|
||||
rand = "0.7"
|
||||
env_logger = "0.7"
|
||||
actix = { version = "0.11.0-beta.2", default-features = false }
|
||||
rand = "0.8"
|
||||
env_logger = "0.8"
|
||||
serde_derive = "1.0"
|
||||
brotli2 = "0.3.2"
|
||||
flate2 = "1.0.13"
|
||||
@ -119,10 +124,11 @@ codegen-units = 1
|
||||
[patch.crates-io]
|
||||
actix-web = { path = "." }
|
||||
actix-http = { path = "actix-http" }
|
||||
actix-http-test = { path = "test-server" }
|
||||
actix-http-test = { path = "actix-http-test" }
|
||||
actix-web-actors = { path = "actix-web-actors" }
|
||||
actix-web-codegen = { path = "actix-web-codegen" }
|
||||
actix-files = { path = "actix-files" }
|
||||
actix-multipart = { path = "actix-multipart" }
|
||||
actix-files = { path = "actix-files" }
|
||||
awc = { path = "awc" }
|
||||
|
||||
[[bench]]
|
||||
@ -132,3 +138,7 @@ harness = false
|
||||
[[bench]]
|
||||
name = "service"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "responder"
|
||||
harness = false
|
||||
|
@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-NOW Nikolay Kim
|
||||
Copyright 2017-NOW Actix Team
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017 Nikolay Kim
|
||||
Copyright (c) 2017 Actix Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
53
MIGRATION.md
53
MIGRATION.md
@ -1,17 +1,70 @@
|
||||
## Unreleased
|
||||
|
||||
* The default `NormalizePath` behavior now strips trailing slashes by default. This was
|
||||
previously documented to be the case in v3 but the behavior now matches. The effect is that
|
||||
routes defined with trailing slashes will become inaccessible when
|
||||
using `NormalizePath::default()`.
|
||||
|
||||
Before: `#[get("/test/")`
|
||||
After: `#[get("/test")`
|
||||
|
||||
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
|
||||
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
|
||||
simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
|
||||
|
||||
* Cookie handling has been offloaded to the `cookie` crate:
|
||||
* `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
|
||||
* Some types now require lifetime parameters.
|
||||
|
||||
* The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
|
||||
any `actix-web` method previously expecting a time v0.1 input.
|
||||
|
||||
* Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
||||
result in `SameSite=None` being sent with the response Set-Cookie header.
|
||||
To create a cookie without a SameSite attribute, remove any calls setting same_site.
|
||||
|
||||
* actix-http support for Actors messages was moved to actix-http crate and is enabled
|
||||
with feature `actors`
|
||||
|
||||
* content_length function is removed from actix-http.
|
||||
You can set Content-Length by normally setting the response body or calling no_chunking function.
|
||||
|
||||
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||
`u64` instead of a `usize`.
|
||||
|
||||
* Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use
|
||||
destructuring or `.into_inner()`. For example:
|
||||
|
||||
```rust
|
||||
// Previously:
|
||||
async fn some_route(path: web::Path<(String, String)>) -> String {
|
||||
format!("Hello, {} {}", path.0, path.1)
|
||||
}
|
||||
|
||||
// Now (this also worked before):
|
||||
async fn some_route(path: web::Path<(String, String)>) -> String {
|
||||
let (first_name, last_name) = path.into_inner();
|
||||
format!("Hello, {} {}", first_name, last_name)
|
||||
}
|
||||
// Or (this wasn't previously supported):
|
||||
async fn some_route(web::Path((first_name, last_name)): web::Path<(String, String)>) -> String {
|
||||
format!("Hello, {} {}", first_name, last_name)
|
||||
}
|
||||
```
|
||||
|
||||
* `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
|
||||
It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`,
|
||||
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
|
||||
|
||||
* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.
|
||||
|
||||
* `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`.
|
||||
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
|
||||
|
99
README.md
99
README.md
@ -1,50 +1,46 @@
|
||||
<div align="center">
|
||||
<p><h1>Actix web</h1> </p>
|
||||
<p><strong>Actix web is a small, pragmatic, and extremely fast rust web framework</strong> </p>
|
||||
<h1>Actix Web</h1>
|
||||
<p>
|
||||
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
[](https://travis-ci.org/actix/actix-web)
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.2)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.2)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions)
|
||||
[](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://docs.rs/actix-web)
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html)
|
||||

|
||||

|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
</p>
|
||||
|
||||
<h3>
|
||||
<a href="https://actix.rs">Website</a>
|
||||
<span> | </span>
|
||||
<a href="https://gitter.im/actix/actix">Chat</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/actix/examples">Examples</a>
|
||||
</h3>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
Actix web is a simple, pragmatic and extremely fast web framework for Rust.
|
||||
## Features
|
||||
|
||||
* Supported *HTTP/1.x* and *HTTP/2.0* protocols
|
||||
* Supports *HTTP/1.x* and *HTTP/2*
|
||||
* Streaming and pipelining
|
||||
* Keep-alive and slow requests handling
|
||||
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
||||
* Transparent content compression/decompression (br, gzip, deflate)
|
||||
* Configurable [request routing](https://actix.rs/docs/url-dispatch/)
|
||||
* Powerful [request routing](https://actix.rs/docs/url-dispatch/)
|
||||
* Multipart streams
|
||||
* Static assets
|
||||
* SSL support with OpenSSL or Rustls
|
||||
* SSL support using OpenSSL or Rustls
|
||||
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||
* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
|
||||
* Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
|
||||
* Supports [Actix actor framework](https://github.com/actix/actix)
|
||||
* Supports Rust 1.40+
|
||||
* Runs on stable Rust 1.46+
|
||||
|
||||
## Docs
|
||||
## Documentation
|
||||
|
||||
* [API documentation (master)](https://actix.rs/actix-web/actix_web)
|
||||
* [API documentation (docs.rs)](https://docs.rs/actix-web)
|
||||
* [User guide](https://actix.rs)
|
||||
* [Website & User Guide](https://actix.rs)
|
||||
* [Examples Repository](https://github.com/actix/examples)
|
||||
* [API Documentation](https://docs.rs/actix-web)
|
||||
* [API Documentation (master branch)](https://actix.rs/actix-web/actix_web)
|
||||
|
||||
## Example
|
||||
|
||||
@ -52,8 +48,7 @@ Dependencies:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-web = "2"
|
||||
actix-rt = "1"
|
||||
actix-web = "3"
|
||||
```
|
||||
|
||||
Code:
|
||||
@ -62,11 +57,11 @@ Code:
|
||||
use actix_web::{get, web, App, HttpServer, Responder};
|
||||
|
||||
#[get("/{id}/{name}/index.html")]
|
||||
async fn index(info: web::Path<(u32, String)>) -> impl Responder {
|
||||
format!("Hello {}! id:{}", info.1, info.0)
|
||||
async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder {
|
||||
format!("Hello {}! id:{}", name, id)
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(|| App::new().service(index))
|
||||
.bind("127.0.0.1:8080")?
|
||||
@ -77,37 +72,39 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
### More examples
|
||||
|
||||
* [Basics](https://github.com/actix/examples/tree/master/basics/)
|
||||
* [Stateful](https://github.com/actix/examples/tree/master/state/)
|
||||
* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/)
|
||||
* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/)
|
||||
* [Tera](https://github.com/actix/examples/tree/master/template_tera/)
|
||||
* [Askama](https://github.com/actix/examples/tree/master/template_askama/) templates
|
||||
* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/)
|
||||
* [r2d2](https://github.com/actix/examples/tree/master/r2d2/)
|
||||
* [OpenSSL](https://github.com/actix/examples/tree/master/openssl/)
|
||||
* [Rustls](https://github.com/actix/examples/tree/master/rustls/)
|
||||
* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/)
|
||||
* [Json](https://github.com/actix/examples/tree/master/json/)
|
||||
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/)
|
||||
* [Application State](https://github.com/actix/examples/tree/master/state/)
|
||||
* [JSON Handling](https://github.com/actix/examples/tree/master/json/)
|
||||
* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/)
|
||||
* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/)
|
||||
* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/)
|
||||
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/)
|
||||
* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/)
|
||||
* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/)
|
||||
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/)
|
||||
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/)
|
||||
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/)
|
||||
|
||||
You may consider checking out
|
||||
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r18)
|
||||
One of the fastest web frameworks available according to the
|
||||
[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
[http://www.apache.org/licenses/LICENSE-2.0])
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||
[http://opensource.org/licenses/MIT])
|
||||
|
||||
at your option.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Contribution to the actix-web crate is organized under the terms of the
|
||||
Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to
|
||||
intervene to uphold that code of conduct.
|
||||
Contribution to the actix-web repo is organized under the terms of the Contributor Covenant.
|
||||
The Actix team promises to intervene to uphold that code of conduct.
|
||||
|
@ -1,11 +0,0 @@
|
||||
# Cors Middleware for actix web framework [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-cors) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
**This crate moved to https://github.com/actix/actix-extras.**
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-cors/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-cors](https://crates.io/crates/actix-cors)
|
||||
* Minimum supported Rust version: 1.34 or later
|
@ -1,81 +1,123 @@
|
||||
# Changes
|
||||
|
||||
## [Unreleased] - 2020-xx-xx
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 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
|
||||
* `HttpRange::parse` now has its own error type.
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
|
||||
|
||||
## 0.5.0 - 2020-12-26
|
||||
* Optionally support hidden files/directories. [#1811]
|
||||
|
||||
[#1811]: https://github.com/actix/actix-web/pull/1811
|
||||
|
||||
|
||||
## 0.4.1 - 2020-11-24
|
||||
* Clarify order of parameters in `Files::new` and improve docs.
|
||||
|
||||
|
||||
## 0.4.0 - 2020-10-06
|
||||
* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
|
||||
|
||||
[#1714]: https://github.com/actix/actix-web/pull/1714
|
||||
|
||||
|
||||
## 0.3.0 - 2020-09-11
|
||||
* No significant changes from 0.3.0-beta.1.
|
||||
|
||||
|
||||
## 0.3.0-beta.1 - 2020-07-15
|
||||
* Update `v_htmlescape` to 0.10
|
||||
* Update `actix-web` and `actix-http` dependencies to beta.1
|
||||
|
||||
|
||||
## 0.3.0-alpha.1 - 2020-05-23
|
||||
* Update `actix-web` and `actix-http` dependencies to alpha
|
||||
* Fix some typos in the docs
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
* Support sending Content-Length when Content-Range is specified #1384
|
||||
* Support sending Content-Length when Content-Range is specified [#1384]
|
||||
|
||||
## [0.2.1] - 2019-12-22
|
||||
[#1384]: https://github.com/actix/actix-web/pull/1384
|
||||
|
||||
|
||||
## 0.2.1 - 2019-12-22
|
||||
* Use the same format for file URLs regardless of platforms
|
||||
|
||||
## [0.2.0] - 2019-12-20
|
||||
|
||||
## 0.2.0 - 2019-12-20
|
||||
* Fix BodyEncoding trait import #1220
|
||||
|
||||
## [0.2.0-alpha.1] - 2019-12-07
|
||||
|
||||
## 0.2.0-alpha.1 - 2019-12-07
|
||||
* Migrate to `std::future`
|
||||
|
||||
## [0.1.7] - 2019-11-06
|
||||
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
|
||||
|
||||
## [0.1.6] - 2019-10-14
|
||||
## 0.1.7 - 2019-11-06
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of
|
||||
`actix_files::NamedFile` to be more compatible. (#1151)
|
||||
|
||||
## 0.1.6 - 2019-10-14
|
||||
* Add option to redirect to a slash-ended path `Files` #1132
|
||||
|
||||
## [0.1.5] - 2019-10-08
|
||||
|
||||
## 0.1.5 - 2019-10-08
|
||||
* Bump up `mime_guess` crate version to 2.0.1
|
||||
|
||||
* Bump up `percent-encoding` crate version to 2.1
|
||||
|
||||
* Allow user defined request guards for `Files` #1113
|
||||
|
||||
## [0.1.4] - 2019-07-20
|
||||
|
||||
## 0.1.4 - 2019-07-20
|
||||
* Allow to disable `Content-Disposition` header #686
|
||||
|
||||
## [0.1.3] - 2019-06-28
|
||||
|
||||
## 0.1.3 - 2019-06-28
|
||||
* Do not set `Content-Length` header, let actix-http set it #930
|
||||
|
||||
## [0.1.2] - 2019-06-13
|
||||
|
||||
## 0.1.2 - 2019-06-13
|
||||
* Content-Length is 0 for NamedFile HEAD request #914
|
||||
|
||||
* Fix ring dependency from actix-web default features for #741
|
||||
|
||||
## [0.1.1] - 2019-06-01
|
||||
|
||||
## 0.1.1 - 2019-06-01
|
||||
* Static files are incorrectly served as both chunked and with length #812
|
||||
|
||||
## [0.1.0] - 2019-05-25
|
||||
|
||||
* NamedFile last-modified check always fails due to nano-seconds
|
||||
in file modified date #820
|
||||
## 0.1.0 - 2019-05-25
|
||||
* NamedFile last-modified check always fails due to nano-seconds in file modified date #820
|
||||
|
||||
## [0.1.0-beta.4] - 2019-05-12
|
||||
|
||||
## 0.1.0-beta.4 - 2019-05-12
|
||||
* Update actix-web to beta.4
|
||||
|
||||
## [0.1.0-beta.1] - 2019-04-20
|
||||
|
||||
## 0.1.0-beta.1 - 2019-04-20
|
||||
* Update actix-web to beta.1
|
||||
|
||||
## [0.1.0-alpha.6] - 2019-04-14
|
||||
|
||||
## 0.1.0-alpha.6 - 2019-04-14
|
||||
* Update actix-web to alpha6
|
||||
|
||||
## [0.1.0-alpha.4] - 2019-04-08
|
||||
|
||||
## 0.1.0-alpha.4 - 2019-04-08
|
||||
* Update actix-web to alpha4
|
||||
|
||||
## [0.1.0-alpha.2] - 2019-04-02
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-04-02
|
||||
* Add default handler support
|
||||
|
||||
## [0.1.0-alpha.1] - 2019-03-28
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
* Initial impl
|
||||
|
@ -1,37 +1,36 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.2.1"
|
||||
version = "0.6.0-beta.2"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Static files support for actix web."
|
||||
description = "Static file serving for Actix Web"
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
documentation = "https://docs.rs/actix-files/"
|
||||
categories = ["asynchronous", "web-programming::http-server"]
|
||||
license = "MIT/Apache-2.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
workspace = ".."
|
||||
|
||||
[lib]
|
||||
name = "actix_files"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "3.0.0-alpha.3", default-features = false }
|
||||
actix-http = "2.0.0-alpha.4"
|
||||
actix-service = "1.0.1"
|
||||
actix-web = { version = "4.0.0-beta.2", default-features = false }
|
||||
actix-service = "2.0.0-beta.4"
|
||||
|
||||
askama_escape = "0.10"
|
||||
bitflags = "1"
|
||||
bytes = "0.5.3"
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
derive_more = "0.99.2"
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
derive_more = "0.99.5"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
v_htmlescape = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] }
|
||||
actix-rt = "2"
|
||||
actix-web = "4.0.0-beta.2"
|
||||
|
@ -1,9 +1,19 @@
|
||||
# Static files support for actix web [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-files) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# actix-files
|
||||
|
||||
## Documentation & community resources
|
||||
> Static file serving for Actix Web
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-files/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-files](https://crates.io/crates/actix-files)
|
||||
* Minimum supported Rust version: 1.40 or later
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.5.0)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.5.0)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-files/)
|
||||
- [Example Project](https://github.com/actix/examples/tree/master/static_index)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum supported Rust version: 1.46 or later
|
||||
|
104
actix-files/src/chunked.rs
Normal file
104
actix-files/src/chunked.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use std::{
|
||||
cmp, fmt,
|
||||
fs::File,
|
||||
future::Future,
|
||||
io::{self, Read, Seek},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_web::{
|
||||
error::{BlockingError, Error},
|
||||
rt::task::{spawn_blocking, JoinHandle},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Stream};
|
||||
|
||||
#[doc(hidden)]
|
||||
/// A helper created from a `std::fs::File` which reads the file
|
||||
/// chunk-by-chunk on a `ThreadPool`.
|
||||
pub struct ChunkedReadFile {
|
||||
size: u64,
|
||||
offset: u64,
|
||||
state: ChunkedReadFileState,
|
||||
counter: u64,
|
||||
}
|
||||
|
||||
enum ChunkedReadFileState {
|
||||
File(Option<File>),
|
||||
Future(JoinHandle<Result<(File, Bytes), io::Error>>),
|
||||
}
|
||||
|
||||
impl ChunkedReadFile {
|
||||
pub(crate) fn new(size: u64, offset: u64, file: File) -> Self {
|
||||
Self {
|
||||
size,
|
||||
offset,
|
||||
state: ChunkedReadFileState::File(Some(file)),
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ChunkedReadFile {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("ChunkedReadFile")
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for ChunkedReadFile {
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let this = self.as_mut().get_mut();
|
||||
match this.state {
|
||||
ChunkedReadFileState::File(ref mut file) => {
|
||||
let size = this.size;
|
||||
let offset = this.offset;
|
||||
let counter = this.counter;
|
||||
|
||||
if size == counter {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let mut file = file
|
||||
.take()
|
||||
.expect("ChunkedReadFile polled after completion");
|
||||
|
||||
let fut = spawn_blocking(move || {
|
||||
let max_bytes =
|
||||
cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
|
||||
let mut buf = Vec::with_capacity(max_bytes);
|
||||
file.seek(io::SeekFrom::Start(offset))?;
|
||||
|
||||
let n_bytes = file
|
||||
.by_ref()
|
||||
.take(max_bytes as u64)
|
||||
.read_to_end(&mut buf)?;
|
||||
|
||||
if n_bytes == 0 {
|
||||
return Err(io::ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
|
||||
Ok((file, Bytes::from(buf)))
|
||||
});
|
||||
this.state = ChunkedReadFileState::Future(fut);
|
||||
self.poll_next(cx)
|
||||
}
|
||||
}
|
||||
ChunkedReadFileState::Future(ref mut fut) => {
|
||||
let (file, bytes) =
|
||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||
this.state = ChunkedReadFileState::File(Some(file));
|
||||
|
||||
this.offset += bytes.len() as u64;
|
||||
this.counter += bytes.len() as u64;
|
||||
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
114
actix-files/src/directory.rs
Normal file
114
actix-files/src/directory.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
|
||||
|
||||
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
|
||||
use askama_escape::{escape as escape_html_entity, Html};
|
||||
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
||||
|
||||
/// A directory; responds with the generated directory listing.
|
||||
#[derive(Debug)]
|
||||
pub struct Directory {
|
||||
/// Base directory.
|
||||
pub base: PathBuf,
|
||||
|
||||
/// Path of subdirectory to generate listing for.
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
/// Create a new directory
|
||||
pub fn new(base: PathBuf, path: PathBuf) -> Directory {
|
||||
Directory { base, path }
|
||||
}
|
||||
|
||||
/// Is this entry visible from this directory?
|
||||
pub fn is_visible(&self, entry: &io::Result<DirEntry>) -> bool {
|
||||
if let Ok(ref entry) = *entry {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.starts_with('.') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Ok(ref md) = entry.metadata() {
|
||||
let ft = md.file_type();
|
||||
return ft.is_dir() || ft.is_file() || ft.is_symlink();
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type DirectoryRenderer =
|
||||
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
|
||||
|
||||
// show file url as relative to static path
|
||||
macro_rules! encode_file_url {
|
||||
($path:ident) => {
|
||||
utf8_percent_encode(&$path, CONTROLS)
|
||||
};
|
||||
}
|
||||
|
||||
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
||||
macro_rules! encode_file_name {
|
||||
($entry:ident) => {
|
||||
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn directory_listing(
|
||||
dir: &Directory,
|
||||
req: &HttpRequest,
|
||||
) -> Result<ServiceResponse, io::Error> {
|
||||
let index_of = format!("Index of {}", req.path());
|
||||
let mut body = String::new();
|
||||
let base = Path::new(req.path());
|
||||
|
||||
for entry in dir.path.read_dir()? {
|
||||
if dir.is_visible(&entry) {
|
||||
let entry = entry.unwrap();
|
||||
let p = match entry.path().strip_prefix(&dir.path) {
|
||||
Ok(p) if cfg!(windows) => {
|
||||
base.join(p).to_string_lossy().replace("\\", "/")
|
||||
}
|
||||
Ok(p) => base.join(p).to_string_lossy().into_owned(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// if file is a directory, add '/' to the end of the name
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
if metadata.is_dir() {
|
||||
let _ = write!(
|
||||
body,
|
||||
"<li><a href=\"{}\">{}/</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
} else {
|
||||
let _ = write!(
|
||||
body,
|
||||
"<li><a href=\"{}\">{}</a></li>",
|
||||
encode_file_url!(p),
|
||||
encode_file_name!(entry),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let html = format!(
|
||||
"<html>\
|
||||
<head><title>{}</title></head>\
|
||||
<body><h1>{}</h1>\
|
||||
<ul>\
|
||||
{}\
|
||||
</ul></body>\n</html>",
|
||||
index_of, index_of, body
|
||||
);
|
||||
Ok(ServiceResponse::new(
|
||||
req.clone(),
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html),
|
||||
))
|
||||
}
|
52
actix-files/src/encoding.rs
Normal file
52
actix-files/src/encoding.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use mime::Mime;
|
||||
|
||||
/// Transforms MIME `text/*` types into their UTF-8 equivalent, if supported.
|
||||
///
|
||||
/// MIME types that are converted
|
||||
/// - application/javascript
|
||||
/// - text/html
|
||||
/// - text/css
|
||||
/// - text/plain
|
||||
/// - text/csv
|
||||
/// - text/tab-separated-values
|
||||
pub(crate) fn equiv_utf8_text(ct: Mime) -> Mime {
|
||||
// use (roughly) order of file-type popularity for a web server
|
||||
|
||||
if ct == mime::APPLICATION_JAVASCRIPT {
|
||||
return mime::APPLICATION_JAVASCRIPT_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_HTML {
|
||||
return mime::TEXT_HTML_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_CSS {
|
||||
return mime::TEXT_CSS_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_PLAIN {
|
||||
return mime::TEXT_PLAIN_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_CSV {
|
||||
return mime::TEXT_CSV_UTF_8;
|
||||
}
|
||||
|
||||
if ct == mime::TEXT_TAB_SEPARATED_VALUES {
|
||||
return mime::TEXT_TAB_SEPARATED_VALUES_UTF_8;
|
||||
}
|
||||
|
||||
ct
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_equiv_utf8_text() {
|
||||
assert_eq!(equiv_utf8_text(mime::TEXT_PLAIN), mime::TEXT_PLAIN_UTF_8);
|
||||
assert_eq!(equiv_utf8_text(mime::TEXT_XML), mime::TEXT_XML);
|
||||
assert_eq!(equiv_utf8_text(mime::IMAGE_PNG), mime::IMAGE_PNG);
|
||||
}
|
||||
}
|
282
actix-files/src/files.rs
Normal file
282
actix-files/src/files.rs
Normal file
@ -0,0 +1,282 @@
|
||||
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
|
||||
|
||||
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
||||
use actix_web::{
|
||||
dev::{
|
||||
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
|
||||
},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::header::DispositionType,
|
||||
HttpRequest,
|
||||
};
|
||||
use futures_util::future::{ok, FutureExt, LocalBoxFuture};
|
||||
|
||||
use crate::{
|
||||
directory_listing, named, Directory, DirectoryRenderer, FilesService,
|
||||
HttpNewService, MimeOverride,
|
||||
};
|
||||
|
||||
/// Static files handling service.
|
||||
///
|
||||
/// `Files` service must be registered with `App::service()` method.
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::App;
|
||||
/// use actix_files::Files;
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .service(Files::new("/static", "."));
|
||||
/// ```
|
||||
pub struct Files {
|
||||
path: String,
|
||||
directory: PathBuf,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
|
||||
renderer: Rc<DirectoryRenderer>,
|
||||
mime_override: Option<Rc<MimeOverride>>,
|
||||
file_flags: named::Flags,
|
||||
guards: Option<Rc<dyn Guard>>,
|
||||
hidden_files: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Files {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Files")
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Files {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
directory: self.directory.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
default: self.default.clone(),
|
||||
renderer: self.renderer.clone(),
|
||||
file_flags: self.file_flags,
|
||||
path: self.path.clone(),
|
||||
mime_override: self.mime_override.clone(),
|
||||
guards: self.guards.clone(),
|
||||
hidden_files: self.hidden_files,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Files {
|
||||
/// Create new `Files` instance for a specified base directory.
|
||||
///
|
||||
/// # Argument Order
|
||||
/// The first argument (`mount_path`) is the root URL at which the static files are served.
|
||||
/// For example, `/assets` will serve files at `example.com/assets/...`.
|
||||
///
|
||||
/// The second argument (`serve_from`) is the location on disk at which files are loaded.
|
||||
/// This can be a relative path. For example, `./` would serve files from the current
|
||||
/// working directory.
|
||||
///
|
||||
/// # Implementation Notes
|
||||
/// If the mount path is set as the root path `/`, services registered after this one will
|
||||
/// be inaccessible. Register more specific handlers and services first.
|
||||
///
|
||||
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a
|
||||
/// max number of threads equal to `512 * HttpServer::worker`. Real time thread count are
|
||||
/// adjusted with work load. More threads would spawn when need and threads goes idle for a
|
||||
/// period of time would be de-spawned.
|
||||
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
|
||||
let orig_dir = serve_from.into();
|
||||
let dir = match orig_dir.canonicalize() {
|
||||
Ok(canon_dir) => canon_dir,
|
||||
Err(_) => {
|
||||
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
||||
PathBuf::new()
|
||||
}
|
||||
};
|
||||
|
||||
Files {
|
||||
path: mount_path.to_owned(),
|
||||
directory: dir,
|
||||
index: None,
|
||||
show_index: false,
|
||||
redirect_to_slash: false,
|
||||
default: Rc::new(RefCell::new(None)),
|
||||
renderer: Rc::new(directory_listing),
|
||||
mime_override: None,
|
||||
file_flags: named::Flags::default(),
|
||||
guards: None,
|
||||
hidden_files: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show files listing for directories.
|
||||
///
|
||||
/// By default show files listing is disabled.
|
||||
pub fn show_files_listing(mut self) -> Self {
|
||||
self.show_index = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Redirects to a slash-ended path when browsing a directory.
|
||||
///
|
||||
/// By default never redirect.
|
||||
pub fn redirect_to_slash_directory(mut self) -> Self {
|
||||
self.redirect_to_slash = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set custom directory renderer
|
||||
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
|
||||
where
|
||||
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
|
||||
+ 'static,
|
||||
{
|
||||
self.renderer = Rc::new(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies mime override callback
|
||||
pub fn mime_override<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
|
||||
{
|
||||
self.mime_override = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index file
|
||||
///
|
||||
/// Shows specific index file for directory "/" instead of
|
||||
/// showing files listing.
|
||||
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
|
||||
self.index = Some(index.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use ETag or not.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
pub fn use_etag(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::ETAG, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use Last-Modified or not.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
pub fn use_last_modified(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::LAST_MD, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether text responses should signal a UTF-8 encoding.
|
||||
///
|
||||
/// Default is false (but will default to true in a future version).
|
||||
#[inline]
|
||||
pub fn prefer_utf8(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::PREFER_UTF8, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies custom guards to use for directory listings and files.
|
||||
///
|
||||
/// Default behaviour allows GET and HEAD.
|
||||
#[inline]
|
||||
pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self {
|
||||
self.guards = Some(Rc::new(guards));
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable `Content-Disposition` header.
|
||||
///
|
||||
/// By default Content-Disposition` header is enabled.
|
||||
#[inline]
|
||||
pub fn disable_content_disposition(mut self) -> Self {
|
||||
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets default handler which is used when no matched file could be found.
|
||||
pub fn default_handler<F, U>(mut self, f: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<U, ServiceRequest>,
|
||||
U: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse,
|
||||
Error = Error,
|
||||
> + 'static,
|
||||
{
|
||||
// create and configure default resource
|
||||
self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
|
||||
f.into_factory().map_init_err(|_| ()),
|
||||
)))));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables serving hidden files and directories, allowing a leading dots in url fragments.
|
||||
#[inline]
|
||||
pub fn use_hidden_files(mut self) -> Self {
|
||||
self.hidden_files = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpServiceFactory for Files {
|
||||
fn register(self, config: &mut AppService) {
|
||||
if self.default.borrow().is_none() {
|
||||
*self.default.borrow_mut() = Some(config.default_service());
|
||||
}
|
||||
|
||||
let rdef = if config.is_root() {
|
||||
ResourceDef::root_prefix(&self.path)
|
||||
} else {
|
||||
ResourceDef::prefix(&self.path)
|
||||
};
|
||||
|
||||
config.register_service(rdef, None, self, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceFactory<ServiceRequest> for Files {
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Config = ();
|
||||
type Service = FilesService;
|
||||
type InitError = ();
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let mut srv = FilesService {
|
||||
directory: self.directory.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
default: None,
|
||||
renderer: self.renderer.clone(),
|
||||
mime_override: self.mime_override.clone(),
|
||||
file_flags: self.file_flags,
|
||||
guards: self.guards.clone(),
|
||||
hidden_files: self.hidden_files,
|
||||
};
|
||||
|
||||
if let Some(ref default) = *self.default.borrow() {
|
||||
default
|
||||
.new_service(())
|
||||
.map(move |result| match result {
|
||||
Ok(default) => {
|
||||
srv.default = Some(default);
|
||||
Ok(srv)
|
||||
}
|
||||
Err(_) => Err(()),
|
||||
})
|
||||
.boxed_local()
|
||||
} else {
|
||||
ok(srv).boxed_local()
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -7,33 +7,35 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use actix_web::{
|
||||
dev::{BodyEncoding, SizedStream},
|
||||
http::{
|
||||
header::{
|
||||
self, Charset, ContentDisposition, DispositionParam, DispositionType,
|
||||
ExtendedValue,
|
||||
},
|
||||
ContentEncoding, StatusCode,
|
||||
},
|
||||
HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||
};
|
||||
use bitflags::bitflags;
|
||||
use mime;
|
||||
use mime_guess::from_path;
|
||||
|
||||
use actix_http::body::SizedStream;
|
||||
use actix_web::dev::BodyEncoding;
|
||||
use actix_web::http::header::{
|
||||
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
|
||||
};
|
||||
use actix_web::http::{ContentEncoding, StatusCode};
|
||||
use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder};
|
||||
use futures_util::future::{ready, Ready};
|
||||
|
||||
use crate::range::HttpRange;
|
||||
use crate::ChunkedReadFile;
|
||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||
|
||||
bitflags! {
|
||||
pub(crate) struct Flags: u8 {
|
||||
const ETAG = 0b0000_0001;
|
||||
const LAST_MD = 0b0000_0010;
|
||||
const ETAG = 0b0000_0001;
|
||||
const LAST_MD = 0b0000_0010;
|
||||
const CONTENT_DISPOSITION = 0b0000_0100;
|
||||
const PREFER_UTF8 = 0b0000_1000;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Flags {
|
||||
fn default() -> Self {
|
||||
Flags::all()
|
||||
Flags::from_bits_truncate(0b0000_0111)
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,12 +92,15 @@ impl NamedFile {
|
||||
};
|
||||
|
||||
let ct = from_path(&path).first_or_octet_stream();
|
||||
let disposition_type = match ct.type_() {
|
||||
|
||||
let disposition = match ct.type_() {
|
||||
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
||||
_ => DispositionType::Attachment,
|
||||
};
|
||||
|
||||
let mut parameters =
|
||||
vec![DispositionParam::Filename(String::from(filename.as_ref()))];
|
||||
|
||||
if !filename.is_ascii() {
|
||||
parameters.push(DispositionParam::FilenameExt(ExtendedValue {
|
||||
charset: Charset::Ext(String::from("UTF-8")),
|
||||
@ -103,16 +108,19 @@ impl NamedFile {
|
||||
value: filename.into_owned().into_bytes(),
|
||||
}))
|
||||
}
|
||||
|
||||
let cd = ContentDisposition {
|
||||
disposition: disposition_type,
|
||||
parameters: parameters,
|
||||
disposition,
|
||||
parameters,
|
||||
};
|
||||
|
||||
(ct, cd)
|
||||
};
|
||||
|
||||
let md = file.metadata()?;
|
||||
let modified = md.modified().ok();
|
||||
let encoding = None;
|
||||
|
||||
Ok(NamedFile {
|
||||
path,
|
||||
file,
|
||||
@ -184,7 +192,7 @@ impl NamedFile {
|
||||
/// image, and video content types, and `attachment` otherwise, and
|
||||
/// the filename is taken from the path provided in the `open` method
|
||||
/// after converting it to UTF-8 using.
|
||||
/// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy).
|
||||
/// [`std::ffi::OsStr::to_string_lossy`]
|
||||
#[inline]
|
||||
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
||||
self.content_disposition = cd;
|
||||
@ -208,24 +216,33 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
///Specifies whether to use ETag or not.
|
||||
/// Specifies whether to use ETag or not.
|
||||
///
|
||||
///Default is true.
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
pub fn use_etag(mut self, value: bool) -> Self {
|
||||
self.flags.set(Flags::ETAG, value);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
///Specifies whether to use Last-Modified or not.
|
||||
/// Specifies whether to use Last-Modified or not.
|
||||
///
|
||||
///Default is true.
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
pub fn use_last_modified(mut self, value: bool) -> Self {
|
||||
self.flags.set(Flags::LAST_MD, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether text responses should signal a UTF-8 encoding.
|
||||
///
|
||||
/// Default is false (but will default to true in a future version).
|
||||
#[inline]
|
||||
pub fn prefer_utf8(mut self, value: bool) -> Self {
|
||||
self.flags.set(Flags::PREFER_UTF8, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
||||
// This etag format is similar to Apache's.
|
||||
self.modified.as_ref().map(|mtime| {
|
||||
@ -243,6 +260,7 @@ impl NamedFile {
|
||||
let dur = mtime
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("modification time must be after epoch");
|
||||
|
||||
header::EntityTag::strong(format!(
|
||||
"{:x}:{:x}:{:x}:{:x}",
|
||||
ino,
|
||||
@ -257,27 +275,32 @@ impl NamedFile {
|
||||
self.modified.map(|mtime| mtime.into())
|
||||
}
|
||||
|
||||
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
|
||||
/// Creates an `HttpResponse` with file as a streaming body.
|
||||
pub fn into_response(self, req: &HttpRequest) -> HttpResponse {
|
||||
if self.status_code != StatusCode::OK {
|
||||
let mut resp = HttpResponse::build(self.status_code);
|
||||
resp.set(header::ContentType(self.content_type.clone()))
|
||||
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
|
||||
res.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
});
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
resp.encoding(current_encoding);
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
} else {
|
||||
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
||||
}
|
||||
let reader = ChunkedReadFile {
|
||||
size: self.md.len(),
|
||||
offset: 0,
|
||||
file: Some(self.file),
|
||||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
return Ok(resp.streaming(reader));
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
res.insert_header((
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
res.encoding(current_encoding);
|
||||
}
|
||||
|
||||
let reader = ChunkedReadFile::new(self.md.len(), 0, self.file);
|
||||
|
||||
return res.streaming(reader);
|
||||
}
|
||||
|
||||
let etag = if self.flags.contains(Flags::ETAG) {
|
||||
@ -285,6 +308,7 @@ impl NamedFile {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let last_modified = if self.flags.contains(Flags::LAST_MD) {
|
||||
self.last_modified()
|
||||
} else {
|
||||
@ -299,8 +323,9 @@ impl NamedFile {
|
||||
{
|
||||
let t1: SystemTime = m.clone().into();
|
||||
let t2: SystemTime = since.clone().into();
|
||||
|
||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||
(Ok(t1), Ok(t2)) => t1 > t2,
|
||||
(Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(),
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
@ -310,15 +335,16 @@ impl NamedFile {
|
||||
// check last modified
|
||||
let not_modified = if !none_match(etag.as_ref(), req) {
|
||||
true
|
||||
} else if req.headers().contains_key(&header::IF_NONE_MATCH) {
|
||||
} else if req.headers().contains_key(header::IF_NONE_MATCH) {
|
||||
false
|
||||
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
|
||||
(last_modified, req.get_header())
|
||||
{
|
||||
let t1: SystemTime = m.clone().into();
|
||||
let t2: SystemTime = since.clone().into();
|
||||
|
||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||
(Ok(t1), Ok(t2)) => t1 <= t2,
|
||||
(Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(),
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
@ -326,38 +352,48 @@ impl NamedFile {
|
||||
};
|
||||
|
||||
let mut resp = HttpResponse::build(self.status_code);
|
||||
resp.set(header::ContentType(self.content_type.clone()))
|
||||
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
|
||||
res.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
});
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
resp.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
} else {
|
||||
resp.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
||||
}
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
resp.insert_header((
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// default compressing
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
resp.encoding(current_encoding);
|
||||
}
|
||||
|
||||
resp.if_some(last_modified, |lm, resp| {
|
||||
resp.set(header::LastModified(lm));
|
||||
})
|
||||
.if_some(etag, |etag, resp| {
|
||||
resp.set(header::ETag(etag));
|
||||
});
|
||||
if let Some(lm) = last_modified {
|
||||
resp.insert_header((header::LAST_MODIFIED, lm.to_string()));
|
||||
}
|
||||
|
||||
resp.header(header::ACCEPT_RANGES, "bytes");
|
||||
if let Some(etag) = etag {
|
||||
resp.insert_header((header::ETAG, etag.to_string()));
|
||||
}
|
||||
|
||||
resp.insert_header((header::ACCEPT_RANGES, "bytes"));
|
||||
|
||||
let mut length = self.md.len();
|
||||
let mut offset = 0;
|
||||
|
||||
// check for range header
|
||||
if let Some(ranges) = req.headers().get(&header::RANGE) {
|
||||
if let Ok(rangesheader) = ranges.to_str() {
|
||||
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
|
||||
length = rangesvec[0].length;
|
||||
offset = rangesvec[0].start;
|
||||
if let Some(ranges) = req.headers().get(header::RANGE) {
|
||||
if let Ok(ranges_header) = ranges.to_str() {
|
||||
if let Ok(ranges) = HttpRange::parse(ranges_header, length) {
|
||||
length = ranges[0].length;
|
||||
offset = ranges[0].start;
|
||||
|
||||
resp.encoding(ContentEncoding::Identity);
|
||||
resp.header(
|
||||
resp.insert_header((
|
||||
header::CONTENT_RANGE,
|
||||
format!(
|
||||
"bytes {}-{}/{}",
|
||||
@ -365,35 +401,32 @@ impl NamedFile {
|
||||
offset + length - 1,
|
||||
self.md.len()
|
||||
),
|
||||
);
|
||||
));
|
||||
} else {
|
||||
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
|
||||
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
|
||||
resp.insert_header((
|
||||
header::CONTENT_RANGE,
|
||||
format!("bytes */{}", length),
|
||||
));
|
||||
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
|
||||
};
|
||||
} else {
|
||||
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
|
||||
return resp.status(StatusCode::BAD_REQUEST).finish();
|
||||
};
|
||||
};
|
||||
|
||||
if precondition_failed {
|
||||
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
|
||||
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
|
||||
} else if not_modified {
|
||||
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
|
||||
return resp.status(StatusCode::NOT_MODIFIED).finish();
|
||||
}
|
||||
|
||||
let reader = ChunkedReadFile {
|
||||
offset,
|
||||
size: length,
|
||||
file: Some(self.file),
|
||||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
let reader = ChunkedReadFile::new(length, offset, self.file);
|
||||
|
||||
if offset != 0 || length != self.md.len() {
|
||||
resp.status(StatusCode::PARTIAL_CONTENT);
|
||||
}
|
||||
|
||||
Ok(resp.body(SizedStream::new(length, reader)))
|
||||
resp.body(SizedStream::new(length, reader))
|
||||
}
|
||||
}
|
||||
|
||||
@ -415,6 +448,7 @@ impl DerefMut for NamedFile {
|
||||
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
match req.get_header::<header::IfMatch>() {
|
||||
None | Some(header::IfMatch::Any) => true,
|
||||
|
||||
Some(header::IfMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
@ -423,6 +457,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -432,6 +467,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
match req.get_header::<header::IfNoneMatch>() {
|
||||
Some(header::IfNoneMatch::Any) => false,
|
||||
|
||||
Some(header::IfNoneMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
@ -440,17 +476,16 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for NamedFile {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<HttpResponse, Error>>;
|
||||
|
||||
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
||||
ready(self.into_response(req))
|
||||
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
|
||||
self.into_response(req)
|
||||
}
|
||||
}
|
||||
|
119
actix-files/src/path_buf.rs
Normal file
119
actix-files/src/path_buf.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use actix_web::{dev::Payload, FromRequest, HttpRequest};
|
||||
use futures_util::future::{ready, Ready};
|
||||
|
||||
use crate::error::UriSegmentError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PathBufWrap(PathBuf);
|
||||
|
||||
impl FromStr for PathBufWrap {
|
||||
type Err = UriSegmentError;
|
||||
|
||||
fn from_str(path: &str) -> Result<Self, Self::Err> {
|
||||
Self::parse_path(path, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl PathBufWrap {
|
||||
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
|
||||
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
||||
let mut buf = PathBuf::new();
|
||||
|
||||
for segment in path.split('/') {
|
||||
if segment == ".." {
|
||||
buf.pop();
|
||||
} else if !hidden_files && segment.starts_with('.') {
|
||||
return Err(UriSegmentError::BadStart('.'));
|
||||
} else if segment.starts_with('*') {
|
||||
return Err(UriSegmentError::BadStart('*'));
|
||||
} else if segment.ends_with(':') {
|
||||
return Err(UriSegmentError::BadEnd(':'));
|
||||
} else if segment.ends_with('>') {
|
||||
return Err(UriSegmentError::BadEnd('>'));
|
||||
} else if segment.ends_with('<') {
|
||||
return Err(UriSegmentError::BadEnd('<'));
|
||||
} else if segment.is_empty() {
|
||||
continue;
|
||||
} else if cfg!(windows) && segment.contains('\\') {
|
||||
return Err(UriSegmentError::BadChar('\\'));
|
||||
} else {
|
||||
buf.push(segment)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PathBufWrap(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for PathBufWrap {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for PathBufWrap {
|
||||
type Error = UriSegmentError;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
type Config = ();
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(req.match_info().path().parse())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_path_buf() {
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/.tt").map(|t| t.0),
|
||||
Err(UriSegmentError::BadStart('.'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/*tt").map(|t| t.0),
|
||||
Err(UriSegmentError::BadStart('*'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/tt:").map(|t| t.0),
|
||||
Err(UriSegmentError::BadEnd(':'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/tt<").map(|t| t.0),
|
||||
Err(UriSegmentError::BadEnd('<'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/test/tt>").map(|t| t.0),
|
||||
Err(UriSegmentError::BadEnd('>'))
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/seg1/seg2/").unwrap().0,
|
||||
PathBuf::from_iter(vec!["seg1", "seg2"])
|
||||
);
|
||||
assert_eq!(
|
||||
PathBufWrap::from_str("/seg1/../seg2/").unwrap().0,
|
||||
PathBuf::from_iter(vec!["seg2"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_path() {
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/test/.tt", false).map(|t| t.0),
|
||||
Err(UriSegmentError::BadStart('.'))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/test/.tt", true).unwrap().0,
|
||||
PathBuf::from_iter(vec!["test", ".tt"])
|
||||
);
|
||||
}
|
||||
}
|
@ -1,24 +1,33 @@
|
||||
use derive_more::{Display, Error};
|
||||
|
||||
/// HTTP Range header representation.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct HttpRange {
|
||||
/// Start of range.
|
||||
pub start: u64,
|
||||
|
||||
/// Length of range.
|
||||
pub length: u64,
|
||||
}
|
||||
|
||||
static PREFIX: &str = "bytes=";
|
||||
const PREFIX: &str = "bytes=";
|
||||
const PREFIX_LEN: usize = 6;
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
#[display(fmt = "Parse HTTP Range failed")]
|
||||
pub struct ParseRangeErr(#[error(not(source))] ());
|
||||
|
||||
impl HttpRange {
|
||||
/// Parses Range HTTP header string as per RFC 2616.
|
||||
///
|
||||
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
||||
/// `size` is full size of response (file).
|
||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ()> {
|
||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
|
||||
if header.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if !header.starts_with(PREFIX) {
|
||||
return Err(());
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
|
||||
let size_sig = size as i64;
|
||||
@ -31,13 +40,14 @@ impl HttpRange {
|
||||
.map(|ra| {
|
||||
let mut start_end_iter = ra.split('-');
|
||||
|
||||
let start_str = start_end_iter.next().ok_or(())?.trim();
|
||||
let end_str = start_end_iter.next().ok_or(())?.trim();
|
||||
let start_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
|
||||
let end_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
|
||||
|
||||
if start_str.is_empty() {
|
||||
// If no start is specified, end specifies the
|
||||
// range start relative to the end of the file.
|
||||
let mut length: i64 = end_str.parse().map_err(|_| ())?;
|
||||
let mut length: i64 =
|
||||
end_str.parse().map_err(|_| ParseRangeErr(()))?;
|
||||
|
||||
if length > size_sig {
|
||||
length = size_sig;
|
||||
@ -48,10 +58,10 @@ impl HttpRange {
|
||||
length: length as u64,
|
||||
}))
|
||||
} else {
|
||||
let start: i64 = start_str.parse().map_err(|_| ())?;
|
||||
let start: i64 = start_str.parse().map_err(|_| ParseRangeErr(()))?;
|
||||
|
||||
if start < 0 {
|
||||
return Err(());
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
if start >= size_sig {
|
||||
no_overlap = true;
|
||||
@ -62,10 +72,11 @@ impl HttpRange {
|
||||
// If no end is specified, range extends to end of the file.
|
||||
size_sig - start
|
||||
} else {
|
||||
let mut end: i64 = end_str.parse().map_err(|_| ())?;
|
||||
let mut end: i64 =
|
||||
end_str.parse().map_err(|_| ParseRangeErr(()))?;
|
||||
|
||||
if start > end {
|
||||
return Err(());
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
|
||||
if end >= size_sig {
|
||||
@ -86,7 +97,7 @@ impl HttpRange {
|
||||
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
|
||||
|
||||
if no_overlap && ranges.is_empty() {
|
||||
return Err(());
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
@ -330,8 +341,7 @@ mod tests {
|
||||
if expected.is_empty() {
|
||||
continue;
|
||||
} else {
|
||||
assert!(
|
||||
false,
|
||||
panic!(
|
||||
"parse({}, {}) returned error {:?}",
|
||||
header,
|
||||
size,
|
||||
@ -343,28 +353,24 @@ mod tests {
|
||||
let got = res.unwrap();
|
||||
|
||||
if got.len() != expected.len() {
|
||||
assert!(
|
||||
false,
|
||||
panic!(
|
||||
"len(parseRange({}, {})) = {}, want {}",
|
||||
header,
|
||||
size,
|
||||
got.len(),
|
||||
expected.len()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
for i in 0..expected.len() {
|
||||
if got[i].start != expected[i].start {
|
||||
assert!(
|
||||
false,
|
||||
panic!(
|
||||
"parseRange({}, {})[{}].start = {}, want {}",
|
||||
header, size, i, got[i].start, expected[i].start
|
||||
)
|
||||
}
|
||||
if got[i].length != expected[i].length {
|
||||
assert!(
|
||||
false,
|
||||
panic!(
|
||||
"parseRange({}, {})[{}].length = {}, want {}",
|
||||
header, size, i, got[i].length, expected[i].length
|
||||
)
|
||||
|
155
actix-files/src/service.rs
Normal file
155
actix-files/src/service.rs
Normal file
@ -0,0 +1,155 @@
|
||||
use std::{fmt, io, path::PathBuf, rc::Rc, task::Poll};
|
||||
|
||||
use actix_service::Service;
|
||||
use actix_web::{
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::{header, Method},
|
||||
HttpResponse,
|
||||
};
|
||||
use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
|
||||
|
||||
use crate::{
|
||||
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride,
|
||||
NamedFile, PathBufWrap,
|
||||
};
|
||||
|
||||
/// Assembled file serving service.
|
||||
pub struct FilesService {
|
||||
pub(crate) directory: PathBuf,
|
||||
pub(crate) index: Option<String>,
|
||||
pub(crate) show_index: bool,
|
||||
pub(crate) redirect_to_slash: bool,
|
||||
pub(crate) default: Option<HttpService>,
|
||||
pub(crate) renderer: Rc<DirectoryRenderer>,
|
||||
pub(crate) mime_override: Option<Rc<MimeOverride>>,
|
||||
pub(crate) file_flags: named::Flags,
|
||||
pub(crate) guards: Option<Rc<dyn Guard>>,
|
||||
pub(crate) hidden_files: bool,
|
||||
}
|
||||
|
||||
type FilesServiceFuture = Either<
|
||||
Ready<Result<ServiceResponse, Error>>,
|
||||
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
|
||||
>;
|
||||
|
||||
impl FilesService {
|
||||
fn handle_err(&self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
|
||||
log::debug!("Failed to handle {}: {}", req.path(), e);
|
||||
|
||||
if let Some(ref default) = self.default {
|
||||
Either::Right(default.call(req))
|
||||
} else {
|
||||
Either::Left(ok(req.error_response(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FilesService {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("FilesService")
|
||||
}
|
||||
}
|
||||
|
||||
impl Service<ServiceRequest> for FilesService {
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Future = FilesServiceFuture;
|
||||
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let is_method_valid = if let Some(guard) = &self.guards {
|
||||
// execute user defined guards
|
||||
(**guard).check(req.head())
|
||||
} else {
|
||||
// default behavior
|
||||
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||
};
|
||||
|
||||
if !is_method_valid {
|
||||
return Either::Left(ok(req.into_response(
|
||||
actix_web::HttpResponse::MethodNotAllowed()
|
||||
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
|
||||
.body("Request did not meet this resource's requirements."),
|
||||
)));
|
||||
}
|
||||
|
||||
let real_path =
|
||||
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Either::Left(ok(req.error_response(e))),
|
||||
};
|
||||
|
||||
// full file path
|
||||
let path = match self.directory.join(&real_path).canonicalize() {
|
||||
Ok(path) => path,
|
||||
Err(e) => return self.handle_err(e, req),
|
||||
};
|
||||
|
||||
if path.is_dir() {
|
||||
if let Some(ref redir_index) = self.index {
|
||||
if self.redirect_to_slash && !req.path().ends_with('/') {
|
||||
let redirect_to = format!("{}/", req.path());
|
||||
|
||||
return Either::Left(ok(req.into_response(
|
||||
HttpResponse::Found()
|
||||
.insert_header((header::LOCATION, redirect_to))
|
||||
.body("")
|
||||
.into_body(),
|
||||
)));
|
||||
}
|
||||
|
||||
let path = path.join(redir_index);
|
||||
|
||||
match NamedFile::open(path) {
|
||||
Ok(mut named_file) => {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition =
|
||||
mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
named_file.flags = self.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let res = named_file.into_response(&req);
|
||||
Either::Left(ok(ServiceResponse::new(req, res)))
|
||||
}
|
||||
Err(e) => self.handle_err(e, req),
|
||||
}
|
||||
} else if self.show_index {
|
||||
let dir = Directory::new(self.directory.clone(), path);
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let x = (self.renderer)(&dir, &req);
|
||||
|
||||
match x {
|
||||
Ok(resp) => Either::Left(ok(resp)),
|
||||
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
|
||||
}
|
||||
} else {
|
||||
Either::Left(ok(ServiceResponse::from_err(
|
||||
FilesError::IsDirectory,
|
||||
req.into_parts().0,
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
match NamedFile::open(path) {
|
||||
Ok(mut named_file) => {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition =
|
||||
mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
named_file.flags = self.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let res = named_file.into_response(&req);
|
||||
Either::Left(ok(ServiceResponse::new(req, res)))
|
||||
}
|
||||
Err(e) => self.handle_err(e, req),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
actix-files/tests/encoding.rs
Normal file
39
actix-files/tests/encoding.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use actix_files::Files;
|
||||
use actix_web::{
|
||||
http::{
|
||||
header::{self, HeaderValue},
|
||||
StatusCode,
|
||||
},
|
||||
test::{self, TestRequest},
|
||||
App,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_utf8_file_contents() {
|
||||
// use default ISO-8859-1 encoding
|
||||
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||
|
||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("text/plain")),
|
||||
);
|
||||
|
||||
// prefer UTF-8 encoding
|
||||
let srv = test::init_service(
|
||||
App::new().service(Files::new("/", "./tests").prefer_utf8(true)),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
|
||||
);
|
||||
}
|
3
actix-files/tests/utf8.txt
Normal file
3
actix-files/tests/utf8.txt
Normal file
@ -0,0 +1,3 @@
|
||||
中文内容显示正确。
|
||||
|
||||
English is OK.
|
@ -1,37 +0,0 @@
|
||||
[package]
|
||||
name = "actix-framed"
|
||||
version = "0.3.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix framed app server"
|
||||
readme = "README.md"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
documentation = "https://docs.rs/actix-framed/"
|
||||
categories = ["network-programming", "asynchronous",
|
||||
"web-programming::http-server",
|
||||
"web-programming::websocket"]
|
||||
license = "MIT/Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "actix_framed"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.2.0"
|
||||
actix-service = "1.0.1"
|
||||
actix-router = "0.2.1"
|
||||
actix-rt = "1.0.0"
|
||||
actix-http = "2.0.0-alpha.4"
|
||||
|
||||
bytes = "0.5.3"
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
pin-project = "0.4.6"
|
||||
log = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-server = "1.0.0"
|
||||
actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] }
|
||||
actix-utils = "1.0.3"
|
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-NOW Nikolay Kim
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2017 Nikolay Kim
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
@ -1,8 +0,0 @@
|
||||
# Framed app for actix web [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-framed) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [API Documentation](https://docs.rs/actix-framed/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-framed](https://crates.io/crates/actix-framed)
|
||||
* Minimum supported Rust version: 1.40 or later
|
@ -1,28 +0,0 @@
|
||||
# Changes
|
||||
|
||||
## [Unreleased] - 2020-xx-xx
|
||||
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
|
||||
## [0.3.0] - 2019-12-25
|
||||
|
||||
* Migrate to actix-http 1.0
|
||||
|
||||
## [0.2.1] - 2019-07-20
|
||||
|
||||
* Remove unneeded actix-utils dependency
|
||||
|
||||
|
||||
## [0.2.0] - 2019-05-12
|
||||
|
||||
* Update dependencies
|
||||
|
||||
|
||||
## [0.1.0] - 2019-04-16
|
||||
|
||||
* Update tests
|
||||
|
||||
|
||||
## [0.1.0-alpha.1] - 2019-04-12
|
||||
|
||||
* Initial release
|
@ -1,221 +0,0 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_http::h1::{Codec, SendResponse};
|
||||
use actix_http::{Error, Request, Response};
|
||||
use actix_router::{Path, Router, Url};
|
||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||
use futures_util::future::{ok, FutureExt, LocalBoxFuture};
|
||||
|
||||
use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService};
|
||||
use crate::request::FramedRequest;
|
||||
use crate::state::State;
|
||||
|
||||
type BoxedResponse = LocalBoxFuture<'static, Result<(), Error>>;
|
||||
|
||||
pub trait HttpServiceFactory {
|
||||
type Factory: ServiceFactory;
|
||||
|
||||
fn path(&self) -> &str;
|
||||
|
||||
fn create(self) -> Self::Factory;
|
||||
}
|
||||
|
||||
/// Application builder
|
||||
pub struct FramedApp<T, S = ()> {
|
||||
state: State<S>,
|
||||
services: Vec<(String, BoxedHttpNewService<FramedRequest<T, S>>)>,
|
||||
}
|
||||
|
||||
impl<T: 'static> FramedApp<T, ()> {
|
||||
pub fn new() -> Self {
|
||||
FramedApp {
|
||||
state: State::new(()),
|
||||
services: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static, S: 'static> FramedApp<T, S> {
|
||||
pub fn with(state: S) -> FramedApp<T, S> {
|
||||
FramedApp {
|
||||
services: Vec::new(),
|
||||
state: State::new(state),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn service<U>(mut self, factory: U) -> Self
|
||||
where
|
||||
U: HttpServiceFactory,
|
||||
U::Factory: ServiceFactory<
|
||||
Config = (),
|
||||
Request = FramedRequest<T, S>,
|
||||
Response = (),
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
> + 'static,
|
||||
<U::Factory as ServiceFactory>::Future: 'static,
|
||||
<U::Factory as ServiceFactory>::Service: Service<
|
||||
Request = FramedRequest<T, S>,
|
||||
Response = (),
|
||||
Error = Error,
|
||||
Future = LocalBoxFuture<'static, Result<(), Error>>,
|
||||
>,
|
||||
{
|
||||
let path = factory.path().to_string();
|
||||
self.services
|
||||
.push((path, Box::new(HttpNewService::new(factory.create()))));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> IntoServiceFactory<FramedAppFactory<T, S>> for FramedApp<T, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
S: 'static,
|
||||
{
|
||||
fn into_factory(self) -> FramedAppFactory<T, S> {
|
||||
FramedAppFactory {
|
||||
state: self.state,
|
||||
services: Rc::new(self.services),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FramedAppFactory<T, S> {
|
||||
state: State<S>,
|
||||
services: Rc<Vec<(String, BoxedHttpNewService<FramedRequest<T, S>>)>>,
|
||||
}
|
||||
|
||||
impl<T, S> ServiceFactory for FramedAppFactory<T, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
S: 'static,
|
||||
{
|
||||
type Config = ();
|
||||
type Request = (Request, Framed<T, Codec>);
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type InitError = ();
|
||||
type Service = FramedAppService<T, S>;
|
||||
type Future = CreateService<T, S>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
CreateService {
|
||||
fut: self
|
||||
.services
|
||||
.iter()
|
||||
.map(|(path, service)| {
|
||||
CreateServiceItem::Future(
|
||||
Some(path.clone()),
|
||||
service.new_service(()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
state: self.state.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct CreateService<T, S> {
|
||||
fut: Vec<CreateServiceItem<T, S>>,
|
||||
state: State<S>,
|
||||
}
|
||||
|
||||
enum CreateServiceItem<T, S> {
|
||||
Future(
|
||||
Option<String>,
|
||||
LocalBoxFuture<'static, Result<BoxedHttpService<FramedRequest<T, S>>, ()>>,
|
||||
),
|
||||
Service(String, BoxedHttpService<FramedRequest<T, S>>),
|
||||
}
|
||||
|
||||
impl<S: 'static, T: 'static> Future for CreateService<T, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Output = Result<FramedAppService<T, S>, ()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let mut done = true;
|
||||
|
||||
// poll http services
|
||||
for item in &mut self.fut {
|
||||
let res = match item {
|
||||
CreateServiceItem::Future(ref mut path, ref mut fut) => {
|
||||
match Pin::new(fut).poll(cx) {
|
||||
Poll::Ready(Ok(service)) => {
|
||||
Some((path.take().unwrap(), service))
|
||||
}
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
Poll::Pending => {
|
||||
done = false;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
CreateServiceItem::Service(_, _) => continue,
|
||||
};
|
||||
|
||||
if let Some((path, service)) = res {
|
||||
*item = CreateServiceItem::Service(path, service);
|
||||
}
|
||||
}
|
||||
|
||||
if done {
|
||||
let router = self
|
||||
.fut
|
||||
.drain(..)
|
||||
.fold(Router::build(), |mut router, item| {
|
||||
match item {
|
||||
CreateServiceItem::Service(path, service) => {
|
||||
router.path(&path, service);
|
||||
}
|
||||
CreateServiceItem::Future(_, _) => unreachable!(),
|
||||
}
|
||||
router
|
||||
});
|
||||
Poll::Ready(Ok(FramedAppService {
|
||||
router: router.finish(),
|
||||
state: self.state.clone(),
|
||||
}))
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FramedAppService<T, S> {
|
||||
state: State<S>,
|
||||
router: Router<BoxedHttpService<FramedRequest<T, S>>>,
|
||||
}
|
||||
|
||||
impl<S: 'static, T: 'static> Service for FramedAppService<T, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Request = (Request, Framed<T, Codec>);
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Future = BoxedResponse;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, (req, framed): (Request, Framed<T, Codec>)) -> Self::Future {
|
||||
let mut path = Path::new(Url::new(req.uri().clone()));
|
||||
|
||||
if let Some((srv, _info)) = self.router.recognize_mut(&mut path) {
|
||||
return srv.call(FramedRequest::new(req, framed, path, self.state.clone()));
|
||||
}
|
||||
SendResponse::new(framed, Response::NotFound().finish())
|
||||
.then(|_| ok(()))
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_http::Error;
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use futures_util::future::{FutureExt, LocalBoxFuture};
|
||||
|
||||
pub(crate) type BoxedHttpService<Req> = Box<
|
||||
dyn Service<
|
||||
Request = Req,
|
||||
Response = (),
|
||||
Error = Error,
|
||||
Future = LocalBoxFuture<'static, Result<(), Error>>,
|
||||
>,
|
||||
>;
|
||||
|
||||
pub(crate) type BoxedHttpNewService<Req> = Box<
|
||||
dyn ServiceFactory<
|
||||
Config = (),
|
||||
Request = Req,
|
||||
Response = (),
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
Service = BoxedHttpService<Req>,
|
||||
Future = LocalBoxFuture<'static, Result<BoxedHttpService<Req>, ()>>,
|
||||
>,
|
||||
>;
|
||||
|
||||
pub(crate) struct HttpNewService<T: ServiceFactory>(T);
|
||||
|
||||
impl<T> HttpNewService<T>
|
||||
where
|
||||
T: ServiceFactory<Response = (), Error = Error>,
|
||||
T::Response: 'static,
|
||||
T::Future: 'static,
|
||||
T::Service: Service<Future = LocalBoxFuture<'static, Result<(), Error>>> + 'static,
|
||||
<T::Service as Service>::Future: 'static,
|
||||
{
|
||||
pub fn new(service: T) -> Self {
|
||||
HttpNewService(service)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ServiceFactory for HttpNewService<T>
|
||||
where
|
||||
T: ServiceFactory<Config = (), Response = (), Error = Error>,
|
||||
T::Request: 'static,
|
||||
T::Future: 'static,
|
||||
T::Service: Service<Future = LocalBoxFuture<'static, Result<(), Error>>> + 'static,
|
||||
<T::Service as Service>::Future: 'static,
|
||||
{
|
||||
type Config = ();
|
||||
type Request = T::Request;
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type InitError = ();
|
||||
type Service = BoxedHttpService<T::Request>;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, ()>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let fut = self.0.new_service(());
|
||||
|
||||
async move {
|
||||
fut.await.map_err(|_| ()).map(|service| {
|
||||
let service: BoxedHttpService<_> =
|
||||
Box::new(HttpServiceWrapper { service });
|
||||
service
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpServiceWrapper<T: Service> {
|
||||
service: T,
|
||||
}
|
||||
|
||||
impl<T> Service for HttpServiceWrapper<T>
|
||||
where
|
||||
T: Service<
|
||||
Response = (),
|
||||
Future = LocalBoxFuture<'static, Result<(), Error>>,
|
||||
Error = Error,
|
||||
>,
|
||||
T::Request: 'static,
|
||||
{
|
||||
type Request = T::Request;
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<(), Error>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.service.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Self::Request) -> Self::Future {
|
||||
self.service.call(req)
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
#![allow(clippy::type_complexity, clippy::new_without_default, dead_code)]
|
||||
mod app;
|
||||
mod helpers;
|
||||
mod request;
|
||||
mod route;
|
||||
mod service;
|
||||
mod state;
|
||||
pub mod test;
|
||||
|
||||
// re-export for convenience
|
||||
pub use actix_http::{http, Error, HttpMessage, Response, ResponseError};
|
||||
|
||||
pub use self::app::{FramedApp, FramedAppService};
|
||||
pub use self::request::FramedRequest;
|
||||
pub use self::route::FramedRoute;
|
||||
pub use self::service::{SendError, VerifyWebSockets};
|
||||
pub use self::state::State;
|
@ -1,172 +0,0 @@
|
||||
use std::cell::{Ref, RefMut};
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_http::http::{HeaderMap, Method, Uri, Version};
|
||||
use actix_http::{h1::Codec, Extensions, Request, RequestHead};
|
||||
use actix_router::{Path, Url};
|
||||
|
||||
use crate::state::State;
|
||||
|
||||
pub struct FramedRequest<Io, S = ()> {
|
||||
req: Request,
|
||||
framed: Framed<Io, Codec>,
|
||||
state: State<S>,
|
||||
pub(crate) path: Path<Url>,
|
||||
}
|
||||
|
||||
impl<Io, S> FramedRequest<Io, S> {
|
||||
pub fn new(
|
||||
req: Request,
|
||||
framed: Framed<Io, Codec>,
|
||||
path: Path<Url>,
|
||||
state: State<S>,
|
||||
) -> Self {
|
||||
Self {
|
||||
req,
|
||||
framed,
|
||||
state,
|
||||
path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io, S> FramedRequest<Io, S> {
|
||||
/// Split request into a parts
|
||||
pub fn into_parts(self) -> (Request, Framed<Io, Codec>, State<S>) {
|
||||
(self.req, self.framed, self.state)
|
||||
}
|
||||
|
||||
/// This method returns reference to the request head
|
||||
#[inline]
|
||||
pub fn head(&self) -> &RequestHead {
|
||||
self.req.head()
|
||||
}
|
||||
|
||||
/// This method returns mutable reference to the request head.
|
||||
/// panics if multiple references of http request exists.
|
||||
#[inline]
|
||||
pub fn head_mut(&mut self) -> &mut RequestHead {
|
||||
self.req.head_mut()
|
||||
}
|
||||
|
||||
/// Shared application state
|
||||
#[inline]
|
||||
pub fn state(&self) -> &S {
|
||||
self.state.get_ref()
|
||||
}
|
||||
|
||||
/// Request's uri.
|
||||
#[inline]
|
||||
pub fn uri(&self) -> &Uri {
|
||||
&self.head().uri
|
||||
}
|
||||
|
||||
/// Read the Request method.
|
||||
#[inline]
|
||||
pub fn method(&self) -> &Method {
|
||||
&self.head().method
|
||||
}
|
||||
|
||||
/// Read the Request Version.
|
||||
#[inline]
|
||||
pub fn version(&self) -> Version {
|
||||
self.head().version
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns request's headers.
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.head().headers
|
||||
}
|
||||
|
||||
/// The target path of this Request.
|
||||
#[inline]
|
||||
pub fn path(&self) -> &str {
|
||||
self.head().uri.path()
|
||||
}
|
||||
|
||||
/// The query string in the URL.
|
||||
///
|
||||
/// E.g., id=10
|
||||
#[inline]
|
||||
pub fn query_string(&self) -> &str {
|
||||
if let Some(query) = self.uri().query().as_ref() {
|
||||
query
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the Path parameters.
|
||||
///
|
||||
/// Params is a container for url parameters.
|
||||
/// A variable segment is specified in the form `{identifier}`,
|
||||
/// where the identifier can be used later in a request handler to
|
||||
/// access the matched value for that segment.
|
||||
#[inline]
|
||||
pub fn match_info(&self) -> &Path<Url> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Request extensions
|
||||
#[inline]
|
||||
pub fn extensions(&self) -> Ref<Extensions> {
|
||||
self.head().extensions()
|
||||
}
|
||||
|
||||
/// Mutable reference to a the request's extensions
|
||||
#[inline]
|
||||
pub fn extensions_mut(&self) -> RefMut<Extensions> {
|
||||
self.head().extensions_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use actix_http::http::{HeaderName, HeaderValue};
|
||||
use actix_http::test::{TestBuffer, TestRequest};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_request() {
|
||||
let buf = TestBuffer::empty();
|
||||
let framed = Framed::new(buf, Codec::default());
|
||||
let req = TestRequest::with_uri("/index.html?q=1")
|
||||
.header("content-type", "test")
|
||||
.finish();
|
||||
let path = Path::new(Url::new(req.uri().clone()));
|
||||
|
||||
let mut freq = FramedRequest::new(req, framed, path, State::new(10u8));
|
||||
assert_eq!(*freq.state(), 10);
|
||||
assert_eq!(freq.version(), Version::HTTP_11);
|
||||
assert_eq!(freq.method(), Method::GET);
|
||||
assert_eq!(freq.path(), "/index.html");
|
||||
assert_eq!(freq.query_string(), "q=1");
|
||||
assert_eq!(
|
||||
freq.headers()
|
||||
.get("content-type")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"test"
|
||||
);
|
||||
|
||||
freq.head_mut().headers.insert(
|
||||
HeaderName::try_from("x-hdr").unwrap(),
|
||||
HeaderValue::from_static("test"),
|
||||
);
|
||||
assert_eq!(
|
||||
freq.headers().get("x-hdr").unwrap().to_str().unwrap(),
|
||||
"test"
|
||||
);
|
||||
|
||||
freq.extensions_mut().insert(100usize);
|
||||
assert_eq!(*freq.extensions().get::<usize>().unwrap(), 100usize);
|
||||
|
||||
let (_, _, state) = freq.into_parts();
|
||||
assert_eq!(*state, 10);
|
||||
}
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_http::{http::Method, Error};
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready};
|
||||
use log::error;
|
||||
|
||||
use crate::app::HttpServiceFactory;
|
||||
use crate::request::FramedRequest;
|
||||
|
||||
/// Resource route definition
|
||||
///
|
||||
/// Route uses builder-like pattern for configuration.
|
||||
/// If handler is not explicitly set, default *404 Not Found* handler is used.
|
||||
pub struct FramedRoute<Io, S, F = (), R = (), E = ()> {
|
||||
handler: F,
|
||||
pattern: String,
|
||||
methods: Vec<Method>,
|
||||
state: PhantomData<(Io, S, R, E)>,
|
||||
}
|
||||
|
||||
impl<Io, S> FramedRoute<Io, S> {
|
||||
pub fn new(pattern: &str) -> Self {
|
||||
FramedRoute {
|
||||
handler: (),
|
||||
pattern: pattern.to_string(),
|
||||
methods: Vec::new(),
|
||||
state: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(path: &str) -> FramedRoute<Io, S> {
|
||||
FramedRoute::new(path).method(Method::GET)
|
||||
}
|
||||
|
||||
pub fn post(path: &str) -> FramedRoute<Io, S> {
|
||||
FramedRoute::new(path).method(Method::POST)
|
||||
}
|
||||
|
||||
pub fn put(path: &str) -> FramedRoute<Io, S> {
|
||||
FramedRoute::new(path).method(Method::PUT)
|
||||
}
|
||||
|
||||
pub fn delete(path: &str) -> FramedRoute<Io, S> {
|
||||
FramedRoute::new(path).method(Method::DELETE)
|
||||
}
|
||||
|
||||
pub fn method(mut self, method: Method) -> Self {
|
||||
self.methods.push(method);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn to<F, R, E>(self, handler: F) -> FramedRoute<Io, S, F, R, E>
|
||||
where
|
||||
F: FnMut(FramedRequest<Io, S>) -> R,
|
||||
R: Future<Output = Result<(), E>> + 'static,
|
||||
|
||||
E: fmt::Debug,
|
||||
{
|
||||
FramedRoute {
|
||||
handler,
|
||||
pattern: self.pattern,
|
||||
methods: self.methods,
|
||||
state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io, S, F, R, E> HttpServiceFactory for FramedRoute<Io, S, F, R, E>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + 'static,
|
||||
F: FnMut(FramedRequest<Io, S>) -> R + Clone,
|
||||
R: Future<Output = Result<(), E>> + 'static,
|
||||
E: fmt::Display,
|
||||
{
|
||||
type Factory = FramedRouteFactory<Io, S, F, R, E>;
|
||||
|
||||
fn path(&self) -> &str {
|
||||
&self.pattern
|
||||
}
|
||||
|
||||
fn create(self) -> Self::Factory {
|
||||
FramedRouteFactory {
|
||||
handler: self.handler,
|
||||
methods: self.methods,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FramedRouteFactory<Io, S, F, R, E> {
|
||||
handler: F,
|
||||
methods: Vec<Method>,
|
||||
_t: PhantomData<(Io, S, R, E)>,
|
||||
}
|
||||
|
||||
impl<Io, S, F, R, E> ServiceFactory for FramedRouteFactory<Io, S, F, R, E>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + 'static,
|
||||
F: FnMut(FramedRequest<Io, S>) -> R + Clone,
|
||||
R: Future<Output = Result<(), E>> + 'static,
|
||||
E: fmt::Display,
|
||||
{
|
||||
type Config = ();
|
||||
type Request = FramedRequest<Io, S>;
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type InitError = ();
|
||||
type Service = FramedRouteService<Io, S, F, R, E>;
|
||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
ok(FramedRouteService {
|
||||
handler: self.handler.clone(),
|
||||
methods: self.methods.clone(),
|
||||
_t: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FramedRouteService<Io, S, F, R, E> {
|
||||
handler: F,
|
||||
methods: Vec<Method>,
|
||||
_t: PhantomData<(Io, S, R, E)>,
|
||||
}
|
||||
|
||||
impl<Io, S, F, R, E> Service for FramedRouteService<Io, S, F, R, E>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + 'static,
|
||||
F: FnMut(FramedRequest<Io, S>) -> R + Clone,
|
||||
R: Future<Output = Result<(), E>> + 'static,
|
||||
E: fmt::Display,
|
||||
{
|
||||
type Request = FramedRequest<Io, S>;
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<(), Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: FramedRequest<Io, S>) -> Self::Future {
|
||||
let fut = (self.handler)(req);
|
||||
|
||||
async move {
|
||||
let res = fut.await;
|
||||
if let Err(e) = res {
|
||||
error!("Error in request handler: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_http::body::BodySize;
|
||||
use actix_http::error::ResponseError;
|
||||
use actix_http::h1::{Codec, Message};
|
||||
use actix_http::ws::{verify_handshake, HandshakeError};
|
||||
use actix_http::{Request, Response};
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use futures_util::future::{err, ok, Either, Ready};
|
||||
|
||||
/// Service that verifies incoming request if it is valid websocket
|
||||
/// upgrade request. In case of error returns `HandshakeError`
|
||||
pub struct VerifyWebSockets<T, C> {
|
||||
_t: PhantomData<(T, C)>,
|
||||
}
|
||||
|
||||
impl<T, C> Default for VerifyWebSockets<T, C> {
|
||||
fn default() -> Self {
|
||||
VerifyWebSockets { _t: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> ServiceFactory for VerifyWebSockets<T, C> {
|
||||
type Config = C;
|
||||
type Request = (Request, Framed<T, Codec>);
|
||||
type Response = (Request, Framed<T, Codec>);
|
||||
type Error = (HandshakeError, Framed<T, Codec>);
|
||||
type InitError = ();
|
||||
type Service = VerifyWebSockets<T, C>;
|
||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: C) -> Self::Future {
|
||||
ok(VerifyWebSockets { _t: PhantomData })
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> Service for VerifyWebSockets<T, C> {
|
||||
type Request = (Request, Framed<T, Codec>);
|
||||
type Response = (Request, Framed<T, Codec>);
|
||||
type Error = (HandshakeError, Framed<T, Codec>);
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, (req, framed): (Request, Framed<T, Codec>)) -> Self::Future {
|
||||
match verify_handshake(req.head()) {
|
||||
Err(e) => err((e, framed)),
|
||||
Ok(_) => ok((req, framed)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send http/1 error response
|
||||
pub struct SendError<T, R, E, C>(PhantomData<(T, R, E, C)>);
|
||||
|
||||
impl<T, R, E, C> Default for SendError<T, R, E, C>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
E: ResponseError,
|
||||
{
|
||||
fn default() -> Self {
|
||||
SendError(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, E, C> ServiceFactory for SendError<T, R, E, C>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
R: 'static,
|
||||
E: ResponseError + 'static,
|
||||
{
|
||||
type Config = C;
|
||||
type Request = Result<R, (E, Framed<T, Codec>)>;
|
||||
type Response = R;
|
||||
type Error = (E, Framed<T, Codec>);
|
||||
type InitError = ();
|
||||
type Service = SendError<T, R, E, C>;
|
||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: C) -> Self::Future {
|
||||
ok(SendError(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, E, C> Service for SendError<T, R, E, C>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
R: 'static,
|
||||
E: ResponseError + 'static,
|
||||
{
|
||||
type Request = Result<R, (E, Framed<T, Codec>)>;
|
||||
type Response = R;
|
||||
type Error = (E, Framed<T, Codec>);
|
||||
type Future = Either<Ready<Result<R, (E, Framed<T, Codec>)>>, SendErrorFut<T, R, E>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Result<R, (E, Framed<T, Codec>)>) -> Self::Future {
|
||||
match req {
|
||||
Ok(r) => Either::Left(ok(r)),
|
||||
Err((e, framed)) => {
|
||||
let res = e.error_response().drop_body();
|
||||
Either::Right(SendErrorFut {
|
||||
framed: Some(framed),
|
||||
res: Some((res, BodySize::Empty).into()),
|
||||
err: Some(e),
|
||||
_t: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pub struct SendErrorFut<T, R, E> {
|
||||
res: Option<Message<(Response<()>, BodySize)>>,
|
||||
framed: Option<Framed<T, Codec>>,
|
||||
err: Option<E>,
|
||||
_t: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<T, R, E> Future for SendErrorFut<T, R, E>
|
||||
where
|
||||
E: ResponseError,
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Output = Result<R, (E, Framed<T, Codec>)>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
if let Some(res) = self.res.take() {
|
||||
if self.framed.as_mut().unwrap().write(res).is_err() {
|
||||
return Poll::Ready(Err((
|
||||
self.err.take().unwrap(),
|
||||
self.framed.take().unwrap(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
match self.framed.as_mut().unwrap().flush(cx) {
|
||||
Poll::Ready(Ok(_)) => {
|
||||
Poll::Ready(Err((self.err.take().unwrap(), self.framed.take().unwrap())))
|
||||
}
|
||||
Poll::Ready(Err(_)) => {
|
||||
Poll::Ready(Err((self.err.take().unwrap(), self.framed.take().unwrap())))
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Application state
|
||||
pub struct State<S>(Arc<S>);
|
||||
|
||||
impl<S> State<S> {
|
||||
pub fn new(state: S) -> State<S> {
|
||||
State(Arc::new(state))
|
||||
}
|
||||
|
||||
pub fn get_ref(&self) -> &S {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Deref for State<S> {
|
||||
type Target = S;
|
||||
|
||||
fn deref(&self) -> &S {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Clone for State<S> {
|
||||
fn clone(&self) -> State<S> {
|
||||
State(self.0.clone())
|
||||
}
|
||||
}
|
@ -1,155 +0,0 @@
|
||||
//! Various helpers for Actix applications to use during testing.
|
||||
use std::convert::TryFrom;
|
||||
use std::future::Future;
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_http::h1::Codec;
|
||||
use actix_http::http::header::{Header, HeaderName, IntoHeaderValue};
|
||||
use actix_http::http::{Error as HttpError, Method, Uri, Version};
|
||||
use actix_http::test::{TestBuffer, TestRequest as HttpTestRequest};
|
||||
use actix_router::{Path, Url};
|
||||
|
||||
use crate::{FramedRequest, State};
|
||||
|
||||
/// Test `Request` builder.
|
||||
pub struct TestRequest<S = ()> {
|
||||
req: HttpTestRequest,
|
||||
path: Path<Url>,
|
||||
state: State<S>,
|
||||
}
|
||||
|
||||
impl Default for TestRequest<()> {
|
||||
fn default() -> TestRequest {
|
||||
TestRequest {
|
||||
req: HttpTestRequest::default(),
|
||||
path: Path::new(Url::new(Uri::default())),
|
||||
state: State::new(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestRequest<()> {
|
||||
/// Create TestRequest and set request uri
|
||||
pub fn with_uri(path: &str) -> Self {
|
||||
Self::get().uri(path)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set header
|
||||
pub fn with_hdr<H: Header>(hdr: H) -> Self {
|
||||
Self::default().set(hdr)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set header
|
||||
pub fn with_header<K, V>(key: K, value: V) -> Self
|
||||
where
|
||||
HeaderName: TryFrom<K>,
|
||||
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
|
||||
V: IntoHeaderValue,
|
||||
{
|
||||
Self::default().header(key, value)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::GET`
|
||||
pub fn get() -> Self {
|
||||
Self::default().method(Method::GET)
|
||||
}
|
||||
|
||||
/// Create TestRequest and set method to `Method::POST`
|
||||
pub fn post() -> Self {
|
||||
Self::default().method(Method::POST)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> TestRequest<S> {
|
||||
/// Create TestRequest and set request uri
|
||||
pub fn with_state(state: S) -> TestRequest<S> {
|
||||
let req = TestRequest::get();
|
||||
TestRequest {
|
||||
state: State::new(state),
|
||||
req: req.req,
|
||||
path: req.path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set HTTP version of this request
|
||||
pub fn version(mut self, ver: Version) -> Self {
|
||||
self.req.version(ver);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set HTTP method of this request
|
||||
pub fn method(mut self, meth: Method) -> Self {
|
||||
self.req.method(meth);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set HTTP Uri of this request
|
||||
pub fn uri(mut self, path: &str) -> Self {
|
||||
self.req.uri(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a header
|
||||
pub fn set<H: Header>(mut self, hdr: H) -> Self {
|
||||
self.req.set(hdr);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a header
|
||||
pub fn header<K, V>(mut self, key: K, value: V) -> Self
|
||||
where
|
||||
HeaderName: TryFrom<K>,
|
||||
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
|
||||
V: IntoHeaderValue,
|
||||
{
|
||||
self.req.header(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set request path pattern parameter
|
||||
pub fn param(mut self, name: &'static str, value: &'static str) -> Self {
|
||||
self.path.add_static(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Complete request creation and generate `Request` instance
|
||||
pub fn finish(mut self) -> FramedRequest<TestBuffer, S> {
|
||||
let req = self.req.finish();
|
||||
self.path.get_mut().update(req.uri());
|
||||
let framed = Framed::new(TestBuffer::empty(), Codec::default());
|
||||
FramedRequest::new(req, framed, self.path, self.state)
|
||||
}
|
||||
|
||||
/// This method generates `FramedRequest` instance and executes async handler
|
||||
pub async fn run<F, R, I, E>(self, f: F) -> Result<I, E>
|
||||
where
|
||||
F: FnOnce(FramedRequest<TestBuffer, S>) -> R,
|
||||
R: Future<Output = Result<I, E>>,
|
||||
{
|
||||
f(self.finish()).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let req = TestRequest::with_uri("/index.html")
|
||||
.header("x-test", "test")
|
||||
.param("test", "123")
|
||||
.finish();
|
||||
|
||||
assert_eq!(*req.state(), ());
|
||||
assert_eq!(req.version(), Version::HTTP_11);
|
||||
assert_eq!(req.method(), Method::GET);
|
||||
assert_eq!(req.path(), "/index.html");
|
||||
assert_eq!(req.query_string(), "");
|
||||
assert_eq!(
|
||||
req.headers().get("x-test").unwrap().to_str().unwrap(),
|
||||
"test"
|
||||
);
|
||||
assert_eq!(&req.match_info()["test"], "123");
|
||||
}
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_http::{body, http::StatusCode, ws, Error, HttpService, Response};
|
||||
use actix_http_test::test_server;
|
||||
use actix_service::{pipeline_factory, IntoServiceFactory, ServiceFactory};
|
||||
use actix_utils::framed::Dispatcher;
|
||||
use bytes::Bytes;
|
||||
use futures_util::{future, SinkExt, StreamExt};
|
||||
|
||||
use actix_framed::{FramedApp, FramedRequest, FramedRoute, SendError, VerifyWebSockets};
|
||||
|
||||
async fn ws_service<T: AsyncRead + AsyncWrite>(
|
||||
req: FramedRequest<T>,
|
||||
) -> Result<(), Error> {
|
||||
let (req, mut framed, _) = req.into_parts();
|
||||
let res = ws::handshake(req.head()).unwrap().message_body(());
|
||||
|
||||
framed
|
||||
.send((res, body::BodySize::None).into())
|
||||
.await
|
||||
.unwrap();
|
||||
Dispatcher::new(framed.into_framed(ws::Codec::new()), service)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn service(msg: ws::Frame) -> Result<ws::Message, Error> {
|
||||
let msg = match msg {
|
||||
ws::Frame::Ping(msg) => ws::Message::Pong(msg),
|
||||
ws::Frame::Text(text) => {
|
||||
ws::Message::Text(String::from_utf8_lossy(&text).to_string())
|
||||
}
|
||||
ws::Frame::Binary(bin) => ws::Message::Binary(bin),
|
||||
ws::Frame::Close(reason) => ws::Message::Close(reason),
|
||||
_ => panic!(),
|
||||
};
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_simple() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.upgrade(
|
||||
FramedApp::new().service(FramedRoute::get("/index.html").to(ws_service)),
|
||||
)
|
||||
.finish(|_| future::ok::<_, Error>(Response::NotFound()))
|
||||
.tcp()
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(srv.ws_at("/test").await.is_err());
|
||||
|
||||
// client service
|
||||
let mut framed = srv.ws_at("/index.html").await.unwrap();
|
||||
framed
|
||||
.send(ws::Message::Text("text".to_string()))
|
||||
.await
|
||||
.unwrap();
|
||||
let (item, mut framed) = framed.into_future().await;
|
||||
assert_eq!(
|
||||
item.unwrap().unwrap(),
|
||||
ws::Frame::Text(Bytes::from_static(b"text"))
|
||||
);
|
||||
|
||||
framed
|
||||
.send(ws::Message::Binary("text".into()))
|
||||
.await
|
||||
.unwrap();
|
||||
let (item, mut framed) = framed.into_future().await;
|
||||
assert_eq!(
|
||||
item.unwrap().unwrap(),
|
||||
ws::Frame::Binary(Bytes::from_static(b"text"))
|
||||
);
|
||||
|
||||
framed.send(ws::Message::Ping("text".into())).await.unwrap();
|
||||
let (item, mut framed) = framed.into_future().await;
|
||||
assert_eq!(
|
||||
item.unwrap().unwrap(),
|
||||
ws::Frame::Pong("text".to_string().into())
|
||||
);
|
||||
|
||||
framed
|
||||
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (item, _) = framed.into_future().await;
|
||||
assert_eq!(
|
||||
item.unwrap().unwrap(),
|
||||
ws::Frame::Close(Some(ws::CloseCode::Normal.into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service() {
|
||||
let mut srv = test_server(|| {
|
||||
pipeline_factory(actix_http::h1::OneRequest::new().map_err(|_| ())).and_then(
|
||||
pipeline_factory(
|
||||
pipeline_factory(VerifyWebSockets::default())
|
||||
.then(SendError::default())
|
||||
.map_err(|_| ()),
|
||||
)
|
||||
.and_then(
|
||||
FramedApp::new()
|
||||
.service(FramedRoute::get("/index.html").to(ws_service))
|
||||
.into_factory()
|
||||
.map_err(|_| ()),
|
||||
),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
// non ws request
|
||||
let res = srv.get("/index.html").send().await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
// not found
|
||||
assert!(srv.ws_at("/test").await.is_err());
|
||||
|
||||
// client service
|
||||
let mut framed = srv.ws_at("/index.html").await.unwrap();
|
||||
framed
|
||||
.send(ws::Message::Text("text".to_string()))
|
||||
.await
|
||||
.unwrap();
|
||||
let (item, mut framed) = framed.into_future().await;
|
||||
assert_eq!(
|
||||
item.unwrap().unwrap(),
|
||||
ws::Frame::Text(Bytes::from_static(b"text"))
|
||||
);
|
||||
|
||||
framed
|
||||
.send(ws::Message::Binary("text".into()))
|
||||
.await
|
||||
.unwrap();
|
||||
let (item, mut framed) = framed.into_future().await;
|
||||
assert_eq!(
|
||||
item.unwrap().unwrap(),
|
||||
ws::Frame::Binary(Bytes::from_static(b"text"))
|
||||
);
|
||||
|
||||
framed.send(ws::Message::Ping("text".into())).await.unwrap();
|
||||
let (item, mut framed) = framed.into_future().await;
|
||||
assert_eq!(
|
||||
item.unwrap().unwrap(),
|
||||
ws::Frame::Pong("text".to_string().into())
|
||||
);
|
||||
|
||||
framed
|
||||
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (item, _) = framed.into_future().await;
|
||||
assert_eq!(
|
||||
item.unwrap().unwrap(),
|
||||
ws::Frame::Close(Some(ws::CloseCode::Normal.into()))
|
||||
);
|
||||
}
|
@ -1,7 +1,32 @@
|
||||
# Changes
|
||||
|
||||
## [2.0.0-alpha.1] - 2020-05-23
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2021-02-10
|
||||
* No notable changes.
|
||||
|
||||
|
||||
## 3.0.0-beta.1 - 2021-01-07
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
|
||||
|
||||
## 2.1.0 - 2020-11-25
|
||||
* Add ability to set address for `TestServer`. [#1645]
|
||||
* Upgrade `base64` to `0.13`.
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1645]: https://github.com/actix/actix-web/pull/1645
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
* Update actix-codec and actix-utils dependencies.
|
||||
|
||||
|
||||
## 2.0.0-alpha.1 - 2020-05-23
|
||||
* Update the `time` dependency to 0.2.7
|
||||
* Update `actix-connect` dependency to 2.0.0-alpha.2
|
||||
* Make `test_server` `async` fn.
|
||||
@ -10,74 +35,56 @@
|
||||
* Update `base64` dependency to 0.12
|
||||
* Update `env_logger` dependency to 0.7
|
||||
|
||||
## [1.0.0] - 2019-12-13
|
||||
|
||||
### Changed
|
||||
|
||||
## 1.0.0 - 2019-12-13
|
||||
* Replaced `TestServer::start()` with `test_server()`
|
||||
|
||||
|
||||
## [1.0.0-alpha.3] - 2019-12-07
|
||||
|
||||
### Changed
|
||||
|
||||
## 1.0.0-alpha.3 - 2019-12-07
|
||||
* Migrate to `std::future`
|
||||
|
||||
|
||||
## [0.2.5] - 2019-09-17
|
||||
|
||||
### Changed
|
||||
|
||||
## 0.2.5 - 2019-09-17
|
||||
* Update serde_urlencoded to "0.6.1"
|
||||
* Increase TestServerRuntime timeouts from 500ms to 3000ms
|
||||
|
||||
### Fixed
|
||||
|
||||
* Do not override current `System`
|
||||
|
||||
|
||||
## [0.2.4] - 2019-07-18
|
||||
|
||||
## 0.2.4 - 2019-07-18
|
||||
* Update actix-server to 0.6
|
||||
|
||||
## [0.2.3] - 2019-07-16
|
||||
|
||||
## 0.2.3 - 2019-07-16
|
||||
* Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
||||
|
||||
## [0.2.2] - 2019-06-16
|
||||
|
||||
## 0.2.2 - 2019-06-16
|
||||
* Add .put() and .sput() methods
|
||||
|
||||
## [0.2.1] - 2019-06-05
|
||||
|
||||
## 0.2.1 - 2019-06-05
|
||||
* Add license files
|
||||
|
||||
## [0.2.0] - 2019-05-12
|
||||
|
||||
## 0.2.0 - 2019-05-12
|
||||
* Update awc and actix-http deps
|
||||
|
||||
## [0.1.1] - 2019-04-24
|
||||
|
||||
## 0.1.1 - 2019-04-24
|
||||
* Always make new connection for http client
|
||||
|
||||
|
||||
## [0.1.0] - 2019-04-16
|
||||
|
||||
## 0.1.0 - 2019-04-16
|
||||
* No changes
|
||||
|
||||
|
||||
## [0.1.0-alpha.3] - 2019-04-02
|
||||
|
||||
## 0.1.0-alpha.3 - 2019-04-02
|
||||
* Request functions accept path #743
|
||||
|
||||
|
||||
## [0.1.0-alpha.2] - 2019-03-29
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-03-29
|
||||
* Added TestServerRuntime::load_body() method
|
||||
|
||||
* Update actix-http and awc libraries
|
||||
|
||||
|
||||
## [0.1.0-alpha.1] - 2019-03-28
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
* Initial impl
|
55
actix-http-test/Cargo.toml
Normal file
55
actix-http-test/Cargo.toml
Normal file
@ -0,0 +1,55 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0-beta.2"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
readme = "README.md"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
documentation = "https://docs.rs/actix-http-test/"
|
||||
categories = ["network-programming", "asynchronous",
|
||||
"web-programming::http-server",
|
||||
"web-programming::websocket"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
exclude = [".gitignore", ".cargo/config"]
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = []
|
||||
|
||||
[lib]
|
||||
name = "actix_http_test"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# openssl
|
||||
openssl = ["tls-openssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2.0.0-beta.4"
|
||||
actix-codec = "0.4.0-beta.1"
|
||||
actix-tls = "3.0.0-beta.3"
|
||||
actix-utils = "3.0.0-beta.2"
|
||||
actix-rt = "2"
|
||||
actix-server = "2.0.0-beta.3"
|
||||
awc = "3.0.0-beta.2"
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
http = "0.2.2"
|
||||
log = "0.4"
|
||||
socket2 = "0.3"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = "4.0.0-beta.2"
|
||||
actix-http = "3.0.0-beta.2"
|
15
actix-http-test/README.md
Normal file
15
actix-http-test/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# actix-http-test
|
||||
|
||||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/2.1.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-http-test/2.1.0)
|
||||
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http-test)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum Supported Rust Version (MSRV): 1.46.0
|
@ -1,4 +1,12 @@
|
||||
//! Various helpers for Actix applications to use during testing.
|
||||
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
extern crate tls_openssl as openssl;
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::{net, thread, time};
|
||||
|
||||
@ -11,8 +19,6 @@ use futures_core::stream::Stream;
|
||||
use http::Method;
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
|
||||
pub use actix_testing::*;
|
||||
|
||||
/// Start test server
|
||||
///
|
||||
/// `TestServer` is very simple test server that simplify process of writing
|
||||
@ -44,21 +50,32 @@ pub use actix_testing::*;
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
test_server_with_addr(tcp, factory).await
|
||||
}
|
||||
|
||||
/// Start [`test server`](test_server()) on a concrete Address
|
||||
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
tcp: net::TcpListener,
|
||||
factory: F,
|
||||
) -> TestServer {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
// run server in separate thread
|
||||
thread::spawn(move || {
|
||||
let sys = System::new("actix-test-server");
|
||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let sys = System::new();
|
||||
let local_addr = tcp.local_addr().unwrap();
|
||||
|
||||
Server::build()
|
||||
let srv = Server::build()
|
||||
.listen("test", tcp, factory)?
|
||||
.workers(1)
|
||||
.disable_signals()
|
||||
.start();
|
||||
.disable_signals();
|
||||
|
||||
sys.block_on(async {
|
||||
srv.run();
|
||||
tx.send((System::current(), local_addr)).unwrap();
|
||||
});
|
||||
|
||||
tx.send((System::current(), local_addr)).unwrap();
|
||||
sys.run()
|
||||
});
|
||||
|
||||
@ -68,7 +85,7 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
|
||||
let connector = {
|
||||
#[cfg(feature = "openssl")]
|
||||
{
|
||||
use open_ssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||
|
||||
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
builder.set_verify(SslVerifyMode::NONE);
|
||||
@ -90,9 +107,8 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
|
||||
}
|
||||
};
|
||||
|
||||
Client::build().connector(connector).finish()
|
||||
Client::builder().connector(connector).finish()
|
||||
};
|
||||
actix_connect::start_default_resolver().await.unwrap();
|
||||
|
||||
TestServer {
|
||||
addr,
|
@ -1,41 +0,0 @@
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: actix-http
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: stable
|
||||
# Nightly channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: nightly
|
||||
|
||||
# Install Rust and Cargo
|
||||
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
|
||||
install:
|
||||
- ps: >-
|
||||
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw64\bin'
|
||||
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\MinGW\bin'
|
||||
}
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs
|
||||
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
|
||||
# 'cargo test' takes care of building for us, so disable Appveyor's build stage.
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo clean
|
||||
- cargo test
|
@ -1,32 +1,185 @@
|
||||
# Changes
|
||||
|
||||
## [2.0.0-alpha.4] - 2020-05-21
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2021-02-19
|
||||
### 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
|
||||
### Added
|
||||
* Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`.
|
||||
|
||||
### Changed
|
||||
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
|
||||
* Bumped `rand` to `0.8`.
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
* Update `h2` to `0.3`. [#1813]
|
||||
* The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864]
|
||||
|
||||
### Removed
|
||||
* Deprecated `on_connect` methods have been removed. Prefer the new
|
||||
`on_connect_ext` technique. [#1857]
|
||||
* Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
||||
due to deprecate of resolver actor. [#1813]
|
||||
* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
||||
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
||||
error would return as `ConnectError::SslError`. [#1813]
|
||||
* 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
|
||||
[#1857]: https://github.com/actix/actix-web/pull/1857
|
||||
[#1864]: https://github.com/actix/actix-web/pull/1864
|
||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||
|
||||
|
||||
## 2.2.0 - 2020-11-25
|
||||
### Added
|
||||
* HttpResponse builders for 1xx status codes. [#1768]
|
||||
* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
|
||||
* `TryFrom<u16>` and `TryFrom<f32>` for `http::header::Quality`. [#1797]
|
||||
|
||||
### Fixed
|
||||
* Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
|
||||
|
||||
### Changed
|
||||
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
|
||||
|
||||
[#1773]: https://github.com/actix/actix-web/pull/1773
|
||||
[#1767]: https://github.com/actix/actix-web/pull/1767
|
||||
[#1768]: https://github.com/actix/actix-web/pull/1768
|
||||
[#1793]: https://github.com/actix/actix-web/pull/1793
|
||||
[#1797]: https://github.com/actix/actix-web/pull/1797
|
||||
|
||||
|
||||
## 2.1.0 - 2020-10-30
|
||||
### Added
|
||||
* Added more flexible `on_connect_ext` methods for on-connect handling. [#1754]
|
||||
|
||||
### Changed
|
||||
* Upgrade `base64` to `0.13`. [#1744]
|
||||
* Upgrade `pin-project` to `1.0`. [#1733]
|
||||
* Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760]
|
||||
|
||||
[#1760]: https://github.com/actix/actix-web/pull/1760
|
||||
[#1754]: https://github.com/actix/actix-web/pull/1754
|
||||
[#1733]: https://github.com/actix/actix-web/pull/1733
|
||||
[#1744]: https://github.com/actix/actix-web/pull/1744
|
||||
|
||||
|
||||
## 2.0.0 - 2020-09-11
|
||||
* No significant changes from `2.0.0-beta.4`.
|
||||
|
||||
|
||||
## 2.0.0-beta.4 - 2020-09-09
|
||||
### Changed
|
||||
* Update actix-codec and actix-utils dependencies.
|
||||
* Update actix-connect and actix-tls dependencies.
|
||||
|
||||
|
||||
## 2.0.0-beta.3 - 2020-08-14
|
||||
### Fixed
|
||||
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
|
||||
|
||||
[#1626]: https://github.com/actix/actix-web/pull/1626
|
||||
|
||||
|
||||
## 2.0.0-beta.2 - 2020-07-21
|
||||
### Fixed
|
||||
* Potential UB in h1 decoder using uninitialized memory. [#1614]
|
||||
|
||||
### Changed
|
||||
* Fix illegal chunked encoding. [#1615]
|
||||
|
||||
[#1614]: https://github.com/actix/actix-web/pull/1614
|
||||
[#1615]: https://github.com/actix/actix-web/pull/1615
|
||||
|
||||
|
||||
## 2.0.0-beta.1 - 2020-07-11
|
||||
### Changed
|
||||
* Migrate cookie handling to `cookie` crate. [#1558]
|
||||
* Update `sha-1` to 0.9. [#1586]
|
||||
* Fix leak in client pool. [#1580]
|
||||
* MSRV is now 1.41.1.
|
||||
|
||||
[#1558]: https://github.com/actix/actix-web/pull/1558
|
||||
[#1586]: https://github.com/actix/actix-web/pull/1586
|
||||
[#1580]: https://github.com/actix/actix-web/pull/1580
|
||||
|
||||
|
||||
## 2.0.0-alpha.4 - 2020-05-21
|
||||
### Changed
|
||||
* 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
|
||||
`u64` instead of a `usize`.
|
||||
* Update `base64` dependency to 0.12
|
||||
|
||||
### Fixed
|
||||
|
||||
* Support parsing of `SameSite=None` [#1503]
|
||||
|
||||
[#1439]: https://github.com/actix/actix-web/pull/1439
|
||||
[#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
|
||||
|
||||
* Correct spelling of ConnectError::Unresolved [#1487]
|
||||
* Fix a mistake in the encoding of websocket continuation messages wherein
|
||||
Item::FirstText and Item::FirstBinary are each encoded as the other.
|
||||
|
||||
### Changed
|
||||
|
||||
* Implement `std::error::Error` for our custom errors [#1422]
|
||||
* Remove `failure` support for `ResponseError` since that crate
|
||||
will be deprecated in the near future.
|
||||
@ -34,338 +187,247 @@
|
||||
[#1422]: https://github.com/actix/actix-web/pull/1422
|
||||
[#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
|
||||
|
||||
* 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 respectively
|
||||
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]
|
||||
|
||||
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
|
||||
respectively 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 allowing to set max_http_version to limit HTTP version to be used. [#1394]
|
||||
|
||||
[#1394]: https://github.com/actix/actix-web/pull/1394
|
||||
[#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
|
||||
|
||||
* Update the `time` dependency to 0.2.7.
|
||||
* 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.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Allow `SameSite=None` cookies to be sent in a response.
|
||||
|
||||
## [1.0.1] - 2019-12-20
|
||||
|
||||
## 1.0.1 - 2019-12-20
|
||||
### Fixed
|
||||
|
||||
* Poll upgrade service's readiness from HTTP service handlers
|
||||
|
||||
* Replace brotli with brotli2 #1224
|
||||
|
||||
## [1.0.0] - 2019-12-13
|
||||
|
||||
## 1.0.0 - 2019-12-13
|
||||
### Added
|
||||
|
||||
* Add websockets continuation frame support
|
||||
|
||||
### Changed
|
||||
|
||||
* Replace `flate2-xxx` features with `compress`
|
||||
|
||||
## [1.0.0-alpha.5] - 2019-12-09
|
||||
|
||||
## 1.0.0-alpha.5 - 2019-12-09
|
||||
### Fixed
|
||||
|
||||
* Check `Upgrade` service readiness before calling it
|
||||
|
||||
* Fix buffer remaining capacity calcualtion
|
||||
* Fix buffer remaining capacity calculation
|
||||
|
||||
### Changed
|
||||
|
||||
* 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
|
||||
|
||||
* Add impl ResponseBuilder for Error
|
||||
|
||||
### Changed
|
||||
|
||||
* Use rust based brotli compression library
|
||||
|
||||
## [1.0.0-alpha.3] - 2019-12-07
|
||||
|
||||
## 1.0.0-alpha.3 - 2019-12-07
|
||||
### Changed
|
||||
|
||||
* Migrate to tokio 0.2
|
||||
|
||||
* Migrate to `std::future`
|
||||
|
||||
|
||||
## [0.2.11] - 2019-11-06
|
||||
|
||||
## 0.2.11 - 2019-11-06
|
||||
### Added
|
||||
|
||||
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
|
||||
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
|
||||
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of
|
||||
`actix_files::NamedFile` to be more compatible. (#1151)
|
||||
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
|
||||
|
||||
### Fixed
|
||||
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
|
||||
charset=utf-8` header [#1118]
|
||||
|
||||
* 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
|
||||
|
||||
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead`
|
||||
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
|
||||
with `RequestHead`
|
||||
|
||||
### Fixed
|
||||
|
||||
* h2 will use error response #1080
|
||||
|
||||
* on_connect result isn't added to request extensions for http2 requests #1009
|
||||
|
||||
|
||||
## [0.2.9] - 2019-08-13
|
||||
|
||||
## 0.2.9 - 2019-08-13
|
||||
### Changed
|
||||
|
||||
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
|
||||
|
||||
* Update percent-encoding to 2.1
|
||||
|
||||
* Update serde_urlencoded to 0.6.1
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
|
||||
|
||||
|
||||
## [0.2.8] - 2019-08-01
|
||||
|
||||
## 0.2.8 - 2019-08-01
|
||||
### Added
|
||||
|
||||
* Add `rustls` support
|
||||
|
||||
* Add `Clone` impl for `HeaderMap`
|
||||
|
||||
### Fixed
|
||||
|
||||
* awc client panic #1016
|
||||
|
||||
* Invalid response with compression middleware enabled, but compression-related features disabled #997
|
||||
* Invalid response with compression middleware enabled, but compression-related features
|
||||
disabled #997
|
||||
|
||||
|
||||
## [0.2.7] - 2019-07-18
|
||||
|
||||
## 0.2.7 - 2019-07-18
|
||||
### Added
|
||||
|
||||
* Add support for downcasting response errors #986
|
||||
|
||||
|
||||
## [0.2.6] - 2019-07-17
|
||||
|
||||
## 0.2.6 - 2019-07-17
|
||||
### Changed
|
||||
|
||||
* Replace `ClonableService` with local copy
|
||||
|
||||
* Upgrade `rand` dependency version to 0.7
|
||||
|
||||
|
||||
## [0.2.5] - 2019-06-28
|
||||
|
||||
## 0.2.5 - 2019-06-28
|
||||
### Added
|
||||
|
||||
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
|
||||
|
||||
### Changed
|
||||
|
||||
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
||||
|
||||
* Add `Copy` and `Clone` impls for `ws::Codec`
|
||||
|
||||
|
||||
## [0.2.4] - 2019-06-16
|
||||
|
||||
## 0.2.4 - 2019-06-16
|
||||
### Fixed
|
||||
|
||||
* Do not compress NoContent (204) responses #918
|
||||
|
||||
|
||||
## [0.2.3] - 2019-06-02
|
||||
|
||||
## 0.2.3 - 2019-06-02
|
||||
### Added
|
||||
|
||||
* Debug impl for ResponseBuilder
|
||||
|
||||
* From SizedStream and BodyStream for Body
|
||||
|
||||
### Changed
|
||||
|
||||
* SizedStream uses u64
|
||||
|
||||
|
||||
## [0.2.2] - 2019-05-29
|
||||
|
||||
## 0.2.2 - 2019-05-29
|
||||
### Fixed
|
||||
|
||||
* Parse incoming stream before closing stream on disconnect #868
|
||||
|
||||
|
||||
## [0.2.1] - 2019-05-25
|
||||
|
||||
## 0.2.1 - 2019-05-25
|
||||
### Fixed
|
||||
|
||||
* Handle socket read disconnect
|
||||
|
||||
|
||||
## [0.2.0] - 2019-05-12
|
||||
|
||||
## 0.2.0 - 2019-05-12
|
||||
### Changed
|
||||
|
||||
* Update actix-service to 0.4
|
||||
|
||||
* Expect and upgrade services accept `ServerConfig` config.
|
||||
|
||||
### Deleted
|
||||
|
||||
* `OneRequest` service
|
||||
|
||||
|
||||
## [0.1.5] - 2019-05-04
|
||||
|
||||
## 0.1.5 - 2019-05-04
|
||||
### Fixed
|
||||
|
||||
* Clean up response extensions in response pool #817
|
||||
|
||||
|
||||
## [0.1.4] - 2019-04-24
|
||||
|
||||
## 0.1.4 - 2019-04-24
|
||||
### Added
|
||||
|
||||
* Allow to render h1 request headers in `Camel-Case`
|
||||
|
||||
### Fixed
|
||||
|
||||
* Read until eof for http/1.0 responses #771
|
||||
|
||||
|
||||
## [0.1.3] - 2019-04-23
|
||||
|
||||
## 0.1.3 - 2019-04-23
|
||||
### Fixed
|
||||
|
||||
* Fix http client pool management
|
||||
|
||||
* Fix http client wait queue management #794
|
||||
|
||||
|
||||
## [0.1.2] - 2019-04-23
|
||||
|
||||
## 0.1.2 - 2019-04-23
|
||||
### Fixed
|
||||
|
||||
* Fix BorrowMutError panic in client connector #793
|
||||
|
||||
|
||||
## [0.1.1] - 2019-04-19
|
||||
|
||||
## 0.1.1 - 2019-04-19
|
||||
### Changed
|
||||
|
||||
* Cookie::max_age() accepts value in seconds
|
||||
|
||||
* Cookie::max_age_time() accepts value in time::Duration
|
||||
|
||||
* Allow to specify server address for client connector
|
||||
|
||||
|
||||
## [0.1.0] - 2019-04-16
|
||||
|
||||
## 0.1.0 - 2019-04-16
|
||||
### Added
|
||||
|
||||
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
|
||||
|
||||
### Changed
|
||||
|
||||
* `actix_http::encoding` always available
|
||||
|
||||
* use trust-dns-resolver 0.11.0
|
||||
|
||||
|
||||
## [0.1.0-alpha.5] - 2019-04-12
|
||||
|
||||
## 0.1.0-alpha.5 - 2019-04-12
|
||||
### Added
|
||||
|
||||
* Allow to use custom service for upgrade requests
|
||||
|
||||
* Added `h1::SendResponse` future.
|
||||
|
||||
### Changed
|
||||
|
||||
* MessageBody::length() renamed to MessageBody::size() for consistency
|
||||
|
||||
* ws handshake verification functions take RequestHead instead of Request
|
||||
|
||||
|
||||
## [0.1.0-alpha.4] - 2019-04-08
|
||||
|
||||
## 0.1.0-alpha.4 - 2019-04-08
|
||||
### Added
|
||||
|
||||
* Allow to use custom `Expect` handler
|
||||
|
||||
* Add minimal `std::error::Error` impl for `Error`
|
||||
|
||||
### Changed
|
||||
|
||||
* Export IntoHeaderValue
|
||||
|
||||
* Render error and return as response body
|
||||
|
||||
* Use thread pool for response body comression
|
||||
* Use thread pool for response body compression
|
||||
|
||||
### Deleted
|
||||
|
||||
* Removed PayloadBuffer
|
||||
|
||||
|
||||
## [0.1.0-alpha.3] - 2019-04-02
|
||||
|
||||
## 0.1.0-alpha.3 - 2019-04-02
|
||||
### Added
|
||||
|
||||
* Warn when an unsealed private cookie isn't valid UTF-8
|
||||
|
||||
### Fixed
|
||||
|
||||
* Rust 1.31.0 compatibility
|
||||
|
||||
* Preallocate read buffer for h1 codec
|
||||
|
||||
* Detect socket disconnection during protocol selection
|
||||
|
||||
|
||||
## [0.1.0-alpha.2] - 2019-03-29
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-03-29
|
||||
### Added
|
||||
|
||||
* Added ws::Message::Nop, no-op websockets message
|
||||
|
||||
### Changed
|
||||
|
||||
* Do not use thread pool for decomression if chunk size is smaller than 2048.
|
||||
* Do not use thread pool for decompression 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
|
||||
|
@ -1,46 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fafhrd91@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "2.0.0-alpha.4"
|
||||
version = "3.0.0-beta.2"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix http primitives"
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
@ -11,7 +11,7 @@ documentation = "https://docs.rs/actix-http/"
|
||||
categories = ["network-programming", "asynchronous",
|
||||
"web-programming::http-server",
|
||||
"web-programming::websocket"]
|
||||
license = "MIT/Apache-2.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
@ -25,43 +25,40 @@ path = "src/lib.rs"
|
||||
default = []
|
||||
|
||||
# openssl
|
||||
openssl = ["actix-tls/openssl", "actix-connect/openssl"]
|
||||
openssl = ["actix-tls/openssl"]
|
||||
|
||||
# rustls support
|
||||
rustls = ["actix-tls/rustls", "actix-connect/rustls"]
|
||||
rustls = ["actix-tls/rustls"]
|
||||
|
||||
# enable compressison support
|
||||
compress = ["flate2", "brotli2"]
|
||||
|
||||
# support for secure cookies
|
||||
secure-cookies = ["ring"]
|
||||
secure-cookies = ["cookie/secure"]
|
||||
|
||||
# support for actix Actor messages
|
||||
actors = ["actix"]
|
||||
# trust-dns as client dns resolver
|
||||
trust-dns = ["trust-dns-resolver"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "1.0.5"
|
||||
actix-codec = "0.2.0"
|
||||
actix-connect = "2.0.0-alpha.3"
|
||||
actix-utils = "1.0.6"
|
||||
actix-rt = "1.0.0"
|
||||
actix-threadpool = "0.3.1"
|
||||
actix-tls = { version = "2.0.0-alpha.1", optional = true }
|
||||
actix = { version = "0.10.0-alpha.1", optional = true }
|
||||
actix-service = "2.0.0-beta.4"
|
||||
actix-codec = "0.4.0-beta.1"
|
||||
actix-utils = "3.0.0-beta.2"
|
||||
actix-rt = "2"
|
||||
actix-tls = "3.0.0-beta.2"
|
||||
|
||||
base64 = "0.12"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.2"
|
||||
bytes = "0.5.3"
|
||||
copyless = "0.1.4"
|
||||
derive_more = "0.99.2"
|
||||
either = "1.5.3"
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
cookie = { version = "0.14.1", features = ["percent-encode"] }
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-channel = { version = "0.3.5", default-features = false }
|
||||
futures-core = { version = "0.3.5", default-features = false }
|
||||
futures-util = { version = "0.3.5", default-features = false }
|
||||
fxhash = "0.2.1"
|
||||
h2 = "0.2.1"
|
||||
http = "0.2.0"
|
||||
futures-channel = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||
ahash = "0.7"
|
||||
h2 = "0.3.0"
|
||||
http = "0.2.2"
|
||||
httparse = "1.3"
|
||||
indexmap = "1.3"
|
||||
itoa = "0.4"
|
||||
@ -70,38 +67,41 @@ language-tags = "0.2"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
pin-project = "0.4.6"
|
||||
rand = "0.7"
|
||||
pin-project = "1.0.0"
|
||||
rand = "0.8"
|
||||
regex = "1.3"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
sha-1 = "0.8"
|
||||
sha-1 = "0.9"
|
||||
smallvec = "1.6"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.6.1"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
|
||||
# for secure cookie
|
||||
ring = { version = "0.16.9", optional = true }
|
||||
serde_urlencoded = "0.7"
|
||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
||||
|
||||
# compression
|
||||
brotli2 = { version="0.3.2", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
|
||||
trust-dns-resolver = { version = "0.20.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-server = "1.0.1"
|
||||
actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] }
|
||||
actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] }
|
||||
actix-tls = { version = "2.0.0-alpha.1", features = ["openssl"] }
|
||||
actix-server = "2.0.0-beta.3"
|
||||
actix-http-test = { version = "3.0.0-beta.2", features = ["openssl"] }
|
||||
actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] }
|
||||
criterion = "0.3"
|
||||
env_logger = "0.7"
|
||||
env_logger = "0.8"
|
||||
serde_derive = "1.0"
|
||||
open-ssl = { version="0.10", package = "openssl" }
|
||||
rust-tls = { version="0.17", package = "rustls" }
|
||||
tls-openssl = { version = "0.10", package = "openssl" }
|
||||
tls-rustls = { version = "0.19", package = "rustls" }
|
||||
|
||||
[[bench]]
|
||||
name = "content-length"
|
||||
name = "write-camel-case"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "status-line"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "uninit-headers"
|
||||
harness = false
|
||||
|
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-NOW Nikolay Kim
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
1
actix-http/LICENSE-APACHE
Symbolic link
1
actix-http/LICENSE-APACHE
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE-APACHE
|
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2017 Nikolay Kim
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
1
actix-http/LICENSE-MIT
Symbolic link
1
actix-http/LICENSE-MIT
Symbolic link
@ -0,0 +1 @@
|
||||
../LICENSE-MIT
|
@ -1,24 +1,30 @@
|
||||
# Actix http [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-http) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# actix-http
|
||||
|
||||
Actix http
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
## Documentation & community resources
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.2)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.2)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation](https://docs.rs/actix-http/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-http](https://crates.io/crates/actix-http)
|
||||
* Minimum supported Rust version: 1.40 or later
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum Supported Rust Version (MSRV): 1.46.0
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
// see examples/framed_hello.rs for complete list of used crates.
|
||||
use std::{env, io};
|
||||
|
||||
use actix_http::{HttpService, Response};
|
||||
use actix_server::Server;
|
||||
use futures::future;
|
||||
use futures_util::future;
|
||||
use http::header::HeaderValue;
|
||||
use log::info;
|
||||
|
||||
|
@ -1,291 +0,0 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
use bytes::BytesMut;
|
||||
|
||||
// benchmark sending all requests at the same time
|
||||
fn bench_write_content_length(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_content_length");
|
||||
|
||||
let sizes = [
|
||||
0, 1, 11, 83, 101, 653, 1001, 6323, 10001, 56329, 100001, 123456, 98724245,
|
||||
4294967202,
|
||||
];
|
||||
|
||||
for i in sizes.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_original::write_content_length(i, &mut b)
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_new::write_content_length(i, &mut b)
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("itoa", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_itoa::write_content_length(i, &mut b)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_write_content_length);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _itoa {
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
pub fn write_content_length(n: usize, bytes: &mut BytesMut) {
|
||||
if n == 0 {
|
||||
bytes.put_slice(b"\r\ncontent-length: 0\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut buf = itoa::Buffer::new();
|
||||
|
||||
bytes.put_slice(b"\r\ncontent-length: ");
|
||||
bytes.put_slice(buf.format(n).as_bytes());
|
||||
bytes.put_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
mod _new {
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
const DIGITS_START: u8 = b'0';
|
||||
|
||||
/// NOTE: bytes object has to contain enough space
|
||||
pub fn write_content_length(n: usize, bytes: &mut BytesMut) {
|
||||
if n == 0 {
|
||||
bytes.put_slice(b"\r\ncontent-length: 0\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
bytes.put_slice(b"\r\ncontent-length: ");
|
||||
|
||||
if n < 10 {
|
||||
bytes.put_u8(DIGITS_START + (n as u8));
|
||||
} else if n < 100 {
|
||||
let n = n as u8;
|
||||
|
||||
let d10 = n / 10;
|
||||
let d1 = n % 10;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else if n < 1000 {
|
||||
let n = n as u16;
|
||||
|
||||
let d100 = (n / 100) as u8;
|
||||
let d10 = ((n / 10) % 10) as u8;
|
||||
let d1 = (n % 10) as u8;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d100);
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else if n < 10_000 {
|
||||
let n = n as u16;
|
||||
|
||||
let d1000 = (n / 1000) as u8;
|
||||
let d100 = ((n / 100) % 10) as u8;
|
||||
let d10 = ((n / 10) % 10) as u8;
|
||||
let d1 = (n % 10) as u8;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d1000);
|
||||
bytes.put_u8(DIGITS_START + d100);
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else if n < 100_000 {
|
||||
let n = n as u32;
|
||||
|
||||
let d10000 = (n / 10000) as u8;
|
||||
let d1000 = ((n / 1000) % 10) as u8;
|
||||
let d100 = ((n / 100) % 10) as u8;
|
||||
let d10 = ((n / 10) % 10) as u8;
|
||||
let d1 = (n % 10) as u8;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d10000);
|
||||
bytes.put_u8(DIGITS_START + d1000);
|
||||
bytes.put_u8(DIGITS_START + d100);
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else if n < 1_000_000 {
|
||||
let n = n as u32;
|
||||
|
||||
let d100000 = (n / 100000) as u8;
|
||||
let d10000 = ((n / 10000) % 10) as u8;
|
||||
let d1000 = ((n / 1000) % 10) as u8;
|
||||
let d100 = ((n / 100) % 10) as u8;
|
||||
let d10 = ((n / 10) % 10) as u8;
|
||||
let d1 = (n % 10) as u8;
|
||||
|
||||
bytes.put_u8(DIGITS_START + d100000);
|
||||
bytes.put_u8(DIGITS_START + d10000);
|
||||
bytes.put_u8(DIGITS_START + d1000);
|
||||
bytes.put_u8(DIGITS_START + d100);
|
||||
bytes.put_u8(DIGITS_START + d10);
|
||||
bytes.put_u8(DIGITS_START + d1);
|
||||
} else {
|
||||
write_usize(n, bytes);
|
||||
}
|
||||
|
||||
bytes.put_slice(b"\r\n");
|
||||
}
|
||||
|
||||
fn write_usize(n: usize, bytes: &mut BytesMut) {
|
||||
let mut n = n;
|
||||
|
||||
// 20 chars is max length of a usize (2^64)
|
||||
// digits will be added to the buffer from lsd to msd
|
||||
let mut buf = BytesMut::with_capacity(20);
|
||||
|
||||
while n > 9 {
|
||||
// "pop" the least-significant digit
|
||||
let lsd = (n % 10) as u8;
|
||||
|
||||
// remove the lsd from n
|
||||
n = n / 10;
|
||||
|
||||
buf.put_u8(DIGITS_START + lsd);
|
||||
}
|
||||
|
||||
// put msd to result buffer
|
||||
bytes.put_u8(DIGITS_START + (n as u8));
|
||||
|
||||
// put, in reverse (msd to lsd), remaining digits to buffer
|
||||
for i in (0..buf.len()).rev() {
|
||||
bytes.put_u8(buf[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
use std::{mem, ptr, slice};
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
|
||||
2021222324252627282930313233343536373839\
|
||||
4041424344454647484950515253545556575859\
|
||||
6061626364656667686970717273747576777879\
|
||||
8081828384858687888990919293949596979899";
|
||||
|
||||
/// NOTE: bytes object has to contain enough space
|
||||
pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
|
||||
if n < 10 {
|
||||
let mut buf: [u8; 21] = [
|
||||
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
|
||||
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n',
|
||||
];
|
||||
buf[18] = (n as u8) + b'0';
|
||||
bytes.put_slice(&buf);
|
||||
} else if n < 100 {
|
||||
let mut buf: [u8; 22] = [
|
||||
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
|
||||
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n',
|
||||
];
|
||||
let d1 = n << 1;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
DEC_DIGITS_LUT.as_ptr().add(d1),
|
||||
buf.as_mut_ptr().offset(18),
|
||||
2,
|
||||
);
|
||||
}
|
||||
bytes.put_slice(&buf);
|
||||
} else if n < 1000 {
|
||||
let mut buf: [u8; 23] = [
|
||||
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
|
||||
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r',
|
||||
b'\n',
|
||||
];
|
||||
// decode 2 more chars, if > 2 chars
|
||||
let d1 = (n % 100) << 1;
|
||||
n /= 100;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
DEC_DIGITS_LUT.as_ptr().add(d1),
|
||||
buf.as_mut_ptr().offset(19),
|
||||
2,
|
||||
)
|
||||
};
|
||||
|
||||
// decode last 1
|
||||
buf[18] = (n as u8) + b'0';
|
||||
|
||||
bytes.put_slice(&buf);
|
||||
} else {
|
||||
bytes.put_slice(b"\r\ncontent-length: ");
|
||||
convert_usize(n, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) {
|
||||
let mut curr: isize = 39;
|
||||
let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() };
|
||||
buf[39] = b'\r';
|
||||
buf[40] = b'\n';
|
||||
let buf_ptr = buf.as_mut_ptr();
|
||||
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
|
||||
|
||||
// eagerly decode 4 characters at a time
|
||||
while n >= 10_000 {
|
||||
let rem = (n % 10_000) as isize;
|
||||
n /= 10_000;
|
||||
|
||||
let d1 = (rem / 100) << 1;
|
||||
let d2 = (rem % 100) << 1;
|
||||
curr -= 4;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
|
||||
ptr::copy_nonoverlapping(
|
||||
lut_ptr.offset(d2),
|
||||
buf_ptr.offset(curr + 2),
|
||||
2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// if we reach here numbers are <= 9999, so at most 4 chars long
|
||||
let mut n = n as isize; // possibly reduce 64bit math
|
||||
|
||||
// decode 2 more chars, if > 2 chars
|
||||
if n >= 100 {
|
||||
let d1 = (n % 100) << 1;
|
||||
n /= 100;
|
||||
curr -= 2;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
}
|
||||
|
||||
// decode last 1 or 2 chars
|
||||
if n < 10 {
|
||||
curr -= 1;
|
||||
unsafe {
|
||||
*buf_ptr.offset(curr) = (n as u8) + b'0';
|
||||
}
|
||||
} else {
|
||||
let d1 = n << 1;
|
||||
curr -= 2;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
bytes.extend_from_slice(slice::from_raw_parts(
|
||||
buf_ptr.offset(curr),
|
||||
41 - curr as usize,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
@ -176,7 +176,7 @@ mod _original {
|
||||
buf[5] = b'0';
|
||||
buf[7] = b'9';
|
||||
}
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut curr: isize = 12;
|
||||
|
137
actix-http/benches/uninit-headers.rs
Normal file
137
actix-http/benches/uninit-headers.rs
Normal file
@ -0,0 +1,137 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
use bytes::BytesMut;
|
||||
|
||||
// A Miri run detects UB, seen on this playground:
|
||||
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915
|
||||
|
||||
fn bench_header_parsing(c: &mut Criterion) {
|
||||
c.bench_function("Original (Unsound) [short]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ_SHORT);
|
||||
_original::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("New (safe) [short]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ_SHORT);
|
||||
_new::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("Original (Unsound) [realistic]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ);
|
||||
_original::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("New (safe) [realistic]", |b| {
|
||||
b.iter(|| {
|
||||
let mut buf = BytesMut::from(REQ);
|
||||
_new::parse_headers(&mut buf);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_header_parsing);
|
||||
criterion_main!(benches);
|
||||
|
||||
const MAX_HEADERS: usize = 96;
|
||||
|
||||
const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
||||
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct HeaderIndex {
|
||||
name: (usize, usize),
|
||||
value: (usize, usize),
|
||||
}
|
||||
|
||||
const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
||||
name: (0, 0),
|
||||
value: (0, 0),
|
||||
};
|
||||
|
||||
const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||
|
||||
impl HeaderIndex {
|
||||
fn record(
|
||||
bytes: &[u8],
|
||||
headers: &[httparse::Header<'_>],
|
||||
indices: &mut [HeaderIndex],
|
||||
) {
|
||||
let bytes_ptr = bytes.as_ptr() as usize;
|
||||
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
|
||||
let name_start = header.name.as_ptr() as usize - bytes_ptr;
|
||||
let name_end = name_start + header.name.len();
|
||||
indices.name = (name_start, name_end);
|
||||
let value_start = header.value.as_ptr() as usize - bytes_ptr;
|
||||
let value_end = value_start + header.value.len();
|
||||
indices.value = (value_start, value_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test cases taken from:
|
||||
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
|
||||
|
||||
const REQ_SHORT: &'static [u8] = b"\
|
||||
GET / HTTP/1.0\r\n\
|
||||
Host: example.com\r\n\
|
||||
Cookie: session=60; user_id=1\r\n\r\n";
|
||||
|
||||
const REQ: &'static [u8] = b"\
|
||||
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
|
||||
Host: www.kittyhell.com\r\n\
|
||||
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\
|
||||
Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\
|
||||
Accept-Encoding: gzip,deflate\r\n\
|
||||
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\
|
||||
Keep-Alive: 115\r\n\
|
||||
Connection: keep-alive\r\n\
|
||||
Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n";
|
||||
|
||||
mod _new {
|
||||
use super::*;
|
||||
|
||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||
|
||||
let mut req = httparse::Request::new(&mut parsed);
|
||||
match req.parse(src).unwrap() {
|
||||
httparse::Status::Complete(_len) => {
|
||||
HeaderIndex::record(src, req.headers, &mut headers);
|
||||
req.headers.len()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
use super::*;
|
||||
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
let mut req = httparse::Request::new(&mut parsed);
|
||||
match req.parse(src).unwrap() {
|
||||
httparse::Status::Complete(_len) => {
|
||||
HeaderIndex::record(src, req.headers, &mut headers);
|
||||
req.headers.len()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
89
actix-http/benches/write-camel-case.rs
Normal file
89
actix-http/benches/write-camel-case.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
fn bench_write_camel_case(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_camel_case");
|
||||
|
||||
let names = ["connection", "Transfer-Encoding", "transfer-encoding"];
|
||||
|
||||
for &i in &names {
|
||||
let bts = i.as_bytes();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Original", i), bts, |b, bts| {
|
||||
b.iter(|| {
|
||||
let mut buf = black_box([0; 24]);
|
||||
_original::write_camel_case(black_box(bts), &mut buf)
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
|
||||
b.iter(|| {
|
||||
let mut buf = black_box([0; 24]);
|
||||
_new::write_camel_case(black_box(bts), &mut buf)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_write_camel_case);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _new {
|
||||
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
||||
// first copy entire (potentially wrong) slice to output
|
||||
buffer[..value.len()].copy_from_slice(value);
|
||||
|
||||
let mut iter = value.iter();
|
||||
|
||||
// first character should be uppercase
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[0] = c & 0b1101_1111;
|
||||
}
|
||||
|
||||
// track 1 ahead of the current position since that's the location being assigned to
|
||||
let mut index = 2;
|
||||
|
||||
// remaining characters after hyphens should also be uppercase
|
||||
while let Some(&c) = iter.next() {
|
||||
if c == b'-' {
|
||||
// advance iter by one and uppercase if needed
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[index] = c & 0b1101_1111;
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
||||
let mut index = 0;
|
||||
let key = value;
|
||||
let mut key_iter = key.iter();
|
||||
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
while let Some(c) = key_iter.next() {
|
||||
buffer[index] = *c;
|
||||
index += 1;
|
||||
if *c == b'-' {
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,10 @@ async fn main() -> io::Result<()> {
|
||||
info!("request body: {:?}", body);
|
||||
Ok::<_, Error>(
|
||||
Response::Ok()
|
||||
.header("x-head", HeaderValue::from_static("dummy value!"))
|
||||
.insert_header((
|
||||
"x-head",
|
||||
HeaderValue::from_static("dummy value!"),
|
||||
))
|
||||
.body(body),
|
||||
)
|
||||
})
|
||||
|
@ -15,7 +15,7 @@ async fn handle_request(mut req: Request) -> Result<Response, Error> {
|
||||
|
||||
info!("request body: {:?}", body);
|
||||
Ok(Response::Ok()
|
||||
.header("x-head", HeaderValue::from_static("dummy value!"))
|
||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||
.body(body))
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,10 @@ async fn main() -> io::Result<()> {
|
||||
.finish(|_req| {
|
||||
info!("{:?}", _req);
|
||||
let mut res = Response::Ok();
|
||||
res.header("x-head", HeaderValue::from_static("dummy value!"));
|
||||
res.insert_header((
|
||||
"x-head",
|
||||
HeaderValue::from_static("dummy value!"),
|
||||
));
|
||||
future::ok::<_, ()>(res.body("Hello world!"))
|
||||
})
|
||||
.tcp()
|
||||
|
@ -1,12 +1,10 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{fmt, mem};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::Stream;
|
||||
use futures_util::ready;
|
||||
use pin_project::{pin_project, project};
|
||||
use futures_core::{ready, Stream};
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
@ -21,12 +19,7 @@ pub enum BodySize {
|
||||
|
||||
impl BodySize {
|
||||
pub fn is_eof(&self) -> bool {
|
||||
match self {
|
||||
BodySize::None
|
||||
| BodySize::Empty
|
||||
| BodySize::Sized(0) => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,10 +63,10 @@ impl<T: MessageBody + Unpin> MessageBody for Box<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
#[pin_project(project = ResponseBodyProj)]
|
||||
pub enum ResponseBody<B> {
|
||||
Body(#[pin] B),
|
||||
Other(#[pin] Body),
|
||||
Other(Body),
|
||||
}
|
||||
|
||||
impl ResponseBody<Body> {
|
||||
@ -109,15 +102,13 @@ impl<B: MessageBody> MessageBody for ResponseBody<B> {
|
||||
}
|
||||
}
|
||||
|
||||
#[project]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||
#[project]
|
||||
match self.project() {
|
||||
ResponseBody::Body(body) => body.poll_next(cx),
|
||||
ResponseBody::Other(body) => body.poll_next(cx),
|
||||
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -125,20 +116,17 @@ impl<B: MessageBody> MessageBody for ResponseBody<B> {
|
||||
impl<B: MessageBody> Stream for ResponseBody<B> {
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
#[project]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
#[project]
|
||||
match self.project() {
|
||||
ResponseBody::Body(body) => body.poll_next(cx),
|
||||
ResponseBody::Other(body) => body.poll_next(cx),
|
||||
ResponseBodyProj::Body(body) => body.poll_next(cx),
|
||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
/// Represents various types of http message body.
|
||||
pub enum Body {
|
||||
/// Empty response. `Content-Length` header is not set.
|
||||
@ -173,13 +161,11 @@ impl MessageBody for Body {
|
||||
}
|
||||
}
|
||||
|
||||
#[project]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||
#[project]
|
||||
match self.project() {
|
||||
match self.get_mut() {
|
||||
Body::None => Poll::Ready(None),
|
||||
Body::Empty => Poll::Ready(None),
|
||||
Body::Bytes(ref mut bin) => {
|
||||
@ -190,7 +176,7 @@ impl MessageBody for Body {
|
||||
Poll::Ready(Some(Ok(mem::take(bin))))
|
||||
}
|
||||
}
|
||||
Body::Message(ref mut body) => Pin::new(body.as_mut()).poll_next(cx),
|
||||
Body::Message(body) => Pin::new(&mut **body).poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -198,14 +184,8 @@ impl MessageBody for Body {
|
||||
impl PartialEq for Body {
|
||||
fn eq(&self, other: &Body) -> bool {
|
||||
match *self {
|
||||
Body::None => match *other {
|
||||
Body::None => true,
|
||||
_ => false,
|
||||
},
|
||||
Body::Empty => match *other {
|
||||
Body::Empty => true,
|
||||
_ => false,
|
||||
},
|
||||
Body::None => matches!(*other, Body::None),
|
||||
Body::Empty => matches!(*other, Body::Empty),
|
||||
Body::Bytes(ref b) => match *other {
|
||||
Body::Bytes(ref b2) => b == b2,
|
||||
_ => false,
|
||||
@ -283,12 +263,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> From<BodyStream<S, E>> for Body
|
||||
impl<S, E> From<BodyStream<S>> for Body
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
||||
E: Into<Error> + 'static,
|
||||
{
|
||||
fn from(s: BodyStream<S, E>) -> Body {
|
||||
fn from(s: BodyStream<S>) -> Body {
|
||||
Body::from_message(s)
|
||||
}
|
||||
}
|
||||
@ -384,27 +364,21 @@ impl MessageBody for String {
|
||||
|
||||
/// Type represent streaming body.
|
||||
/// Response does not contain `content-length` header and appropriate transfer encoding is used.
|
||||
#[pin_project]
|
||||
pub struct BodyStream<S: Unpin, E> {
|
||||
#[pin]
|
||||
pub struct BodyStream<S: Unpin> {
|
||||
stream: S,
|
||||
_t: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<S, E> BodyStream<S, E>
|
||||
impl<S, E> BodyStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||
E: Into<Error>,
|
||||
{
|
||||
pub fn new(stream: S) -> Self {
|
||||
BodyStream {
|
||||
stream,
|
||||
_t: PhantomData,
|
||||
}
|
||||
BodyStream { stream }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> MessageBody for BodyStream<S, E>
|
||||
impl<S, E> MessageBody for BodyStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||
E: Into<Error>,
|
||||
@ -419,13 +393,12 @@ where
|
||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||
/// [`Stream`] ends.
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||
let mut stream = self.project().stream;
|
||||
loop {
|
||||
let stream = stream.as_mut();
|
||||
return Poll::Ready(match ready!(stream.poll_next(cx)) {
|
||||
let stream = &mut self.as_mut().stream;
|
||||
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
|
||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||
opt => opt.map(|res| res.map_err(Into::into)),
|
||||
});
|
||||
@ -435,10 +408,8 @@ where
|
||||
|
||||
/// Type represent streaming body. This body implementation should be used
|
||||
/// if total size of stream is known. Data get sent as is without using transfer encoding.
|
||||
#[pin_project]
|
||||
pub struct SizedStream<S: Unpin> {
|
||||
size: u64,
|
||||
#[pin]
|
||||
stream: S,
|
||||
}
|
||||
|
||||
@ -465,13 +436,12 @@ where
|
||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||
/// [`Stream`] ends.
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||
let mut stream: Pin<&mut S> = self.project().stream;
|
||||
loop {
|
||||
let stream = stream.as_mut();
|
||||
return Poll::Ready(match ready!(stream.poll_next(cx)) {
|
||||
let stream = &mut self.as_mut().stream;
|
||||
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
|
||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||
val => val,
|
||||
});
|
||||
@ -482,9 +452,9 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures_util::stream;
|
||||
use futures_util::future::poll_fn;
|
||||
use futures_util::pin_mut;
|
||||
use futures_util::stream;
|
||||
|
||||
impl Body {
|
||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
||||
@ -618,10 +588,6 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_body_eq() {
|
||||
assert!(Body::None == Body::None);
|
||||
assert!(Body::None != Body::Empty);
|
||||
assert!(Body::Empty == Body::Empty);
|
||||
assert!(Body::Empty != Body::None);
|
||||
assert!(
|
||||
Body::Bytes(Bytes::from_static(b"1"))
|
||||
== Body::Bytes(Bytes::from_static(b"1"))
|
||||
@ -633,7 +599,7 @@ mod tests {
|
||||
async fn test_body_debug() {
|
||||
assert!(format!("{:?}", Body::None).contains("Body::None"));
|
||||
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
|
||||
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains("1"));
|
||||
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@ -735,7 +701,7 @@ mod tests {
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast");
|
||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||
body.push_str("!");
|
||||
body.push('!');
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast!");
|
||||
let not_body = resp_body.downcast_ref::<()>();
|
||||
|
@ -10,16 +10,16 @@ use crate::config::{KeepAlive, ServiceConfig};
|
||||
use crate::error::Error;
|
||||
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler};
|
||||
use crate::h2::H2Service;
|
||||
use crate::helpers::{Data, DataFactory};
|
||||
use crate::request::Request;
|
||||
use crate::response::Response;
|
||||
use crate::service::HttpService;
|
||||
use crate::{ConnectCallback, Extensions};
|
||||
|
||||
/// A http service builder
|
||||
/// A HTTP service builder
|
||||
///
|
||||
/// This type can be used to construct an instance of `http service` through a
|
||||
/// This type can be used to construct an instance of [`HttpService`] through a
|
||||
/// builder-like pattern.
|
||||
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
|
||||
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
||||
keep_alive: KeepAlive,
|
||||
client_timeout: u64,
|
||||
client_disconnect: u64,
|
||||
@ -27,16 +27,16 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
_t: PhantomData<(T, S)>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler<T>>
|
||||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
/// Create instance of `ServiceConfigBuilder`
|
||||
pub fn new() -> Self {
|
||||
@ -48,26 +48,26 @@ where
|
||||
local_addr: None,
|
||||
expect: ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect: None,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: None,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
|
||||
where
|
||||
S: ServiceFactory<Config = (), Request = Request>,
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
<X::Service as Service>::Future: 'static,
|
||||
U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
|
||||
<X::Service as Service<Request>>::Future: 'static,
|
||||
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
U::InitError: fmt::Debug,
|
||||
<U::Service as Service>::Future: 'static,
|
||||
<U::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
|
||||
{
|
||||
/// Set server keep-alive setting.
|
||||
///
|
||||
@ -123,11 +123,11 @@ where
|
||||
/// request will be forwarded to main service.
|
||||
pub fn expect<F, X1>(self, expect: F) -> HttpServiceBuilder<T, S, X1, U>
|
||||
where
|
||||
F: IntoServiceFactory<X1>,
|
||||
X1: ServiceFactory<Config = (), Request = Request, Response = Request>,
|
||||
F: IntoServiceFactory<X1, Request>,
|
||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X1::Error: Into<Error>,
|
||||
X1::InitError: fmt::Debug,
|
||||
<X1::Service as Service>::Future: 'static,
|
||||
<X1::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
@ -137,8 +137,8 @@ where
|
||||
local_addr: self.local_addr,
|
||||
expect: expect.into_factory(),
|
||||
upgrade: self.upgrade,
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,15 +148,11 @@ where
|
||||
/// and this service get called with original request and framed object.
|
||||
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
|
||||
where
|
||||
F: IntoServiceFactory<U1>,
|
||||
U1: ServiceFactory<
|
||||
Config = (),
|
||||
Request = (Request, Framed<T, Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
F: IntoServiceFactory<U1, (Request, Framed<T, Codec>)>,
|
||||
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
||||
U1::Error: fmt::Display,
|
||||
U1::InitError: fmt::Debug,
|
||||
<U1::Service as Service>::Future: 'static,
|
||||
<U1::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
@ -166,29 +162,29 @@ where
|
||||
local_addr: self.local_addr,
|
||||
expect: self.expect,
|
||||
upgrade: Some(upgrade.into_factory()),
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set on-connect callback.
|
||||
/// Sets the callback to be run on connection establishment.
|
||||
///
|
||||
/// It get called once per connection and result of the call
|
||||
/// get stored to the request's extensions.
|
||||
pub fn on_connect<F, I>(mut self, f: F) -> Self
|
||||
/// Has mutable access to a data container that will be merged into request extensions.
|
||||
/// This enables transport layer data (like client certificates) to be accessed in middleware
|
||||
/// and handlers.
|
||||
pub fn on_connect_ext<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&T) -> I + 'static,
|
||||
I: Clone + 'static,
|
||||
F: Fn(&T, &mut Extensions) + 'static,
|
||||
{
|
||||
self.on_connect = Some(Rc::new(move |io| Box::new(Data(f(io)))));
|
||||
self.on_connect_ext = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Finish service configuration and create *http service* for HTTP/1 protocol.
|
||||
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol.
|
||||
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
|
||||
where
|
||||
B: MessageBody,
|
||||
F: IntoServiceFactory<S>,
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
@ -200,21 +196,22 @@ where
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
|
||||
H1Service::with_config(cfg, service.into_factory())
|
||||
.expect(self.expect)
|
||||
.upgrade(self.upgrade)
|
||||
.on_connect(self.on_connect)
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create *http service* for HTTP/2 protocol.
|
||||
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
||||
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
F: IntoServiceFactory<S>,
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
@ -223,18 +220,20 @@ where
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
H2Service::with_config(cfg, service.into_factory()).on_connect(self.on_connect)
|
||||
|
||||
H2Service::with_config(cfg, service.into_factory())
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create `HttpService` instance.
|
||||
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
F: IntoServiceFactory<S>,
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
@ -243,9 +242,10 @@ where
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
);
|
||||
|
||||
HttpService::with_config(cfg, service.into_factory())
|
||||
.expect(self.expect)
|
||||
.upgrade(self.upgrade)
|
||||
.on_connect(self.on_connect)
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
// These values are taken from hyper/src/proto/h2/client.rs
|
||||
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2mb
|
||||
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1mb
|
||||
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB
|
||||
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB
|
||||
|
||||
/// Connector configuration
|
||||
#[derive(Clone)]
|
||||
@ -19,7 +18,7 @@ pub(crate) struct ConnectorConfig {
|
||||
impl Default for ConnectorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout: Duration::from_secs(1),
|
||||
timeout: Duration::from_secs(5),
|
||||
conn_lifetime: Duration::from_secs(75),
|
||||
conn_keep_alive: Duration::from_secs(15),
|
||||
disconnect_timeout: Some(Duration::from_millis(3000)),
|
||||
|
@ -1,13 +1,16 @@
|
||||
use std::future::Future;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{fmt, io, mem, time};
|
||||
use std::{fmt, io, time};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use bytes::{Buf, Bytes};
|
||||
use futures_util::future::{err, Either, FutureExt, LocalBoxFuture, Ready};
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
|
||||
use actix_rt::task::JoinHandle;
|
||||
use bytes::Bytes;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures_util::future::{err, Either, FutureExt, Ready};
|
||||
use h2::client::SendRequest;
|
||||
use pin_project::{pin_project, project};
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::body::MessageBody;
|
||||
use crate::h1::ClientCodec;
|
||||
@ -20,7 +23,53 @@ use super::{h1proto, h2proto};
|
||||
|
||||
pub(crate) enum ConnectionType<Io> {
|
||||
H1(Io),
|
||||
H2(SendRequest<Bytes>),
|
||||
H2(H2Connection),
|
||||
}
|
||||
|
||||
// h2 connection has two parts: SendRequest and Connection.
|
||||
// Connection is spawned as async task on runtime and H2Connection would hold a handle for
|
||||
// this task. So it can wake up and quit the task when SendRequest is dropped.
|
||||
pub(crate) struct H2Connection {
|
||||
handle: JoinHandle<()>,
|
||||
sender: SendRequest<Bytes>,
|
||||
}
|
||||
|
||||
impl H2Connection {
|
||||
pub(crate) fn new<Io>(
|
||||
sender: SendRequest<Bytes>,
|
||||
connection: h2::client::Connection<Io>,
|
||||
) -> Self
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
let handle = actix_rt::spawn(async move {
|
||||
let _ = connection.await;
|
||||
});
|
||||
|
||||
Self { handle, sender }
|
||||
}
|
||||
}
|
||||
|
||||
// wake up waker when drop
|
||||
impl Drop for H2Connection {
|
||||
fn drop(&mut self) {
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
// only expose sender type to public.
|
||||
impl Deref for H2Connection {
|
||||
type Target = SendRequest<Bytes>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.sender
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for H2Connection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.sender
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Connection {
|
||||
@ -46,10 +95,10 @@ pub trait Connection {
|
||||
|
||||
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
|
||||
/// Close connection
|
||||
fn close(&mut self);
|
||||
fn close(self: Pin<&mut Self>);
|
||||
|
||||
/// Release connection to the connection pool
|
||||
fn release(&mut self);
|
||||
fn release(self: Pin<&mut Self>);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@ -195,17 +244,21 @@ where
|
||||
match self {
|
||||
EitherConnection::A(con) => con
|
||||
.open_tunnel(head)
|
||||
.map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::A))))
|
||||
.map(|res| {
|
||||
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A)))
|
||||
})
|
||||
.boxed_local(),
|
||||
EitherConnection::B(con) => con
|
||||
.open_tunnel(head)
|
||||
.map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::B))))
|
||||
.map(|res| {
|
||||
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B)))
|
||||
})
|
||||
.boxed_local(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
#[pin_project(project = EitherIoProj)]
|
||||
pub enum EitherIo<A, B> {
|
||||
A(#[pin] A),
|
||||
B(#[pin] B),
|
||||
@ -216,26 +269,14 @@ where
|
||||
A: AsyncRead,
|
||||
B: AsyncRead,
|
||||
{
|
||||
#[project]
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
#[project]
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
match self.project() {
|
||||
EitherIo::A(val) => val.poll_read(cx, buf),
|
||||
EitherIo::B(val) => val.poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn prepare_uninitialized_buffer(
|
||||
&self,
|
||||
buf: &mut [mem::MaybeUninit<u8>],
|
||||
) -> bool {
|
||||
match self {
|
||||
EitherIo::A(ref val) => val.prepare_uninitialized_buffer(buf),
|
||||
EitherIo::B(ref val) => val.prepare_uninitialized_buffer(buf),
|
||||
EitherIoProj::A(val) => val.poll_read(cx, buf),
|
||||
EitherIoProj::B(val) => val.poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -245,53 +286,63 @@ where
|
||||
A: AsyncWrite,
|
||||
B: AsyncWrite,
|
||||
{
|
||||
#[project]
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
#[project]
|
||||
match self.project() {
|
||||
EitherIo::A(val) => val.poll_write(cx, buf),
|
||||
EitherIo::B(val) => val.poll_write(cx, buf),
|
||||
EitherIoProj::A(val) => val.poll_write(cx, buf),
|
||||
EitherIoProj::B(val) => val.poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
#[project]
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
#[project]
|
||||
match self.project() {
|
||||
EitherIo::A(val) => val.poll_flush(cx),
|
||||
EitherIo::B(val) => val.poll_flush(cx),
|
||||
EitherIoProj::A(val) => val.poll_flush(cx),
|
||||
EitherIoProj::B(val) => val.poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
#[project]
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
#[project]
|
||||
match self.project() {
|
||||
EitherIo::A(val) => val.poll_shutdown(cx),
|
||||
EitherIo::B(val) => val.poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
|
||||
#[project]
|
||||
fn poll_write_buf<U: Buf>(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut U,
|
||||
) -> Poll<Result<usize, io::Error>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
#[project]
|
||||
match self.project() {
|
||||
EitherIo::A(val) => val.poll_write_buf(cx, buf),
|
||||
EitherIo::B(val) => val.poll_write_buf(cx, buf),
|
||||
EitherIoProj::A(val) => val.poll_shutdown(cx),
|
||||
EitherIoProj::B(val) => val.poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::net;
|
||||
|
||||
use actix_rt::net::TcpStream;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_connection_drop() {
|
||||
let addr = "127.0.0.1:0".parse::<net::SocketAddr>().unwrap();
|
||||
let listener = net::TcpListener::bind(addr).unwrap();
|
||||
let local = listener.local_addr().unwrap();
|
||||
|
||||
std::thread::spawn(move || while listener.accept().is_ok() {});
|
||||
|
||||
let tcp = TcpStream::connect(local).await.unwrap();
|
||||
let (sender, connection) = h2::client::handshake(tcp).await.unwrap();
|
||||
let conn = H2Connection::new(sender.clone(), connection);
|
||||
|
||||
assert!(sender.clone().ready().await.is_ok());
|
||||
assert!(h2::client::SendRequest::clone(&*conn).ready().await.is_ok());
|
||||
|
||||
drop(conn);
|
||||
|
||||
match sender.ready().await {
|
||||
Ok(_) => panic!("connection should be gone and can not be ready"),
|
||||
Err(e) => assert!(e.is_io()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,11 @@ use std::marker::PhantomData;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_connect::{
|
||||
default_connector, Connect as TcpConnect, Connection as TcpConnection,
|
||||
};
|
||||
use actix_rt::net::TcpStream;
|
||||
use actix_service::{apply_fn, Service};
|
||||
use actix_service::{apply_fn, Service, ServiceExt};
|
||||
use actix_tls::connect::{
|
||||
new_connector, Connect as TcpConnect, Connection as TcpConnection, Resolver,
|
||||
};
|
||||
use actix_utils::timeout::{TimeoutError, TimeoutService};
|
||||
use http::Uri;
|
||||
|
||||
@ -18,10 +18,9 @@ use super::pool::{ConnectionPool, Protocol};
|
||||
use super::Connect;
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
use actix_connect::ssl::openssl::SslConnector as OpensslConnector;
|
||||
|
||||
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
|
||||
#[cfg(feature = "rustls")]
|
||||
use actix_connect::ssl::rustls::ClientConfig;
|
||||
use actix_tls::connect::ssl::rustls::ClientConfig;
|
||||
#[cfg(feature = "rustls")]
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -52,7 +51,7 @@ pub struct Connector<T, U> {
|
||||
config: ConnectorConfig,
|
||||
#[allow(dead_code)]
|
||||
ssl: SslConnector,
|
||||
_t: PhantomData<U>,
|
||||
_phantom: PhantomData<U>,
|
||||
}
|
||||
|
||||
trait Io: AsyncRead + AsyncWrite + Unpin {}
|
||||
@ -62,24 +61,24 @@ impl Connector<(), ()> {
|
||||
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
|
||||
pub fn new() -> Connector<
|
||||
impl Service<
|
||||
Request = TcpConnect<Uri>,
|
||||
TcpConnect<Uri>,
|
||||
Response = TcpConnection<Uri, TcpStream>,
|
||||
Error = actix_connect::ConnectError,
|
||||
Error = actix_tls::connect::ConnectError,
|
||||
> + Clone,
|
||||
TcpStream,
|
||||
> {
|
||||
Connector {
|
||||
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
|
||||
connector: default_connector(),
|
||||
connector: new_connector(resolver::resolver()),
|
||||
config: ConnectorConfig::default(),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
// Build Ssl connector with openssl, based on supplied alpn protocols
|
||||
#[cfg(feature = "openssl")]
|
||||
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
|
||||
use actix_connect::ssl::openssl::SslMethod;
|
||||
use actix_tls::connect::ssl::openssl::SslMethod;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
let mut alpn = BytesMut::with_capacity(20);
|
||||
@ -100,9 +99,9 @@ impl Connector<(), ()> {
|
||||
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::rustls::TLS_SERVER_ROOTS);
|
||||
config.root_store.add_server_trust_anchors(
|
||||
&actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS,
|
||||
);
|
||||
SslConnector::Rustls(Arc::new(config))
|
||||
}
|
||||
|
||||
@ -117,16 +116,16 @@ impl<T, U> Connector<T, U> {
|
||||
where
|
||||
U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug,
|
||||
T1: Service<
|
||||
Request = TcpConnect<Uri>,
|
||||
TcpConnect<Uri>,
|
||||
Response = TcpConnection<Uri, U1>,
|
||||
Error = actix_connect::ConnectError,
|
||||
Error = actix_tls::connect::ConnectError,
|
||||
> + Clone,
|
||||
{
|
||||
Connector {
|
||||
connector,
|
||||
config: self.config,
|
||||
ssl: self.ssl,
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -135,9 +134,9 @@ impl<T, U> Connector<T, U>
|
||||
where
|
||||
U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static,
|
||||
T: Service<
|
||||
Request = TcpConnect<Uri>,
|
||||
TcpConnect<Uri>,
|
||||
Response = TcpConnection<Uri, U>,
|
||||
Error = actix_connect::ConnectError,
|
||||
Error = actix_tls::connect::ConnectError,
|
||||
> + Clone
|
||||
+ 'static,
|
||||
{
|
||||
@ -241,8 +240,8 @@ where
|
||||
/// its combinator chain.
|
||||
pub fn finish(
|
||||
self,
|
||||
) -> impl Service<Request = Connect, Response = impl Connection, Error = ConnectError>
|
||||
+ Clone {
|
||||
) -> impl Service<Connect, Response = impl Connection, Error = ConnectError> + Clone
|
||||
{
|
||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
||||
{
|
||||
let connector = TimeoutService::new(
|
||||
@ -268,11 +267,11 @@ where
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
{
|
||||
const H2: &[u8] = b"h2";
|
||||
#[cfg(feature = "openssl")]
|
||||
use actix_connect::ssl::openssl::OpensslConnector;
|
||||
#[cfg(feature = "rustls")]
|
||||
use actix_connect::ssl::rustls::{RustlsConnector, Session};
|
||||
use actix_service::{boxed::service, pipeline};
|
||||
#[cfg(feature = "openssl")]
|
||||
use actix_tls::connect::ssl::openssl::OpensslConnector;
|
||||
#[cfg(feature = "rustls")]
|
||||
use actix_tls::connect::ssl::rustls::{RustlsConnector, Session};
|
||||
|
||||
let ssl_service = TimeoutService::new(
|
||||
self.config.timeout,
|
||||
@ -363,8 +362,7 @@ mod connect_impl {
|
||||
pub(crate) struct InnerConnector<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
pub(crate) tcp_pool: ConnectionPool<T, Io>,
|
||||
}
|
||||
@ -372,8 +370,7 @@ mod connect_impl {
|
||||
impl<T, Io> Clone for InnerConnector<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
InnerConnector {
|
||||
@ -382,25 +379,23 @@ mod connect_impl {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Io> Service for InnerConnector<T, Io>
|
||||
impl<T, Io> Service<Connect> for InnerConnector<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
type Request = Connect;
|
||||
type Response = IoConnection<Io>;
|
||||
type Error = ConnectError;
|
||||
type Future = Either<
|
||||
<ConnectionPool<T, Io> as Service>::Future,
|
||||
<ConnectionPool<T, Io> as Service<Connect>>::Future,
|
||||
Ready<Result<IoConnection<Io>, ConnectError>>,
|
||||
>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.tcp_pool.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Connect) -> Self::Future {
|
||||
fn call(&self, req: Connect) -> Self::Future {
|
||||
match req.uri.scheme_str() {
|
||||
Some("https") | Some("wss") => {
|
||||
Either::Right(err(ConnectError::SslIsNotSupported))
|
||||
@ -428,8 +423,8 @@ mod connect_impl {
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>,
|
||||
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>,
|
||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
|
||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
|
||||
{
|
||||
pub(crate) tcp_pool: ConnectionPool<T1, Io1>,
|
||||
pub(crate) ssl_pool: ConnectionPool<T2, Io2>,
|
||||
@ -439,10 +434,8 @@ mod connect_impl {
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
InnerConnector {
|
||||
@ -452,16 +445,13 @@ mod connect_impl {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T1, T2, Io1, Io2> Service for InnerConnector<T1, T2, Io1, Io2>
|
||||
impl<T1, T2, Io1, Io2> Service<Connect> for InnerConnector<T1, T2, Io1, Io2>
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
type Request = Connect;
|
||||
type Response = EitherConnection<Io1, Io2>;
|
||||
type Error = ConnectError;
|
||||
type Future = Either<
|
||||
@ -469,19 +459,19 @@ mod connect_impl {
|
||||
InnerConnectorResponseB<T2, Io1, Io2>,
|
||||
>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.tcp_pool.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Connect) -> Self::Future {
|
||||
fn call(&self, req: Connect) -> Self::Future {
|
||||
match req.uri.scheme_str() {
|
||||
Some("https") | Some("wss") => Either::Right(InnerConnectorResponseB {
|
||||
fut: self.ssl_pool.call(req),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
_ => Either::Left(InnerConnectorResponseA {
|
||||
fut: self.tcp_pool.call(req),
|
||||
_t: PhantomData,
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -491,18 +481,16 @@ mod connect_impl {
|
||||
pub(crate) struct InnerConnectorResponseA<T, Io1, Io2>
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
#[pin]
|
||||
fut: <ConnectionPool<T, Io1> as Service>::Future,
|
||||
_t: PhantomData<Io2>,
|
||||
fut: <ConnectionPool<T, Io1> as Service<Connect>>::Future,
|
||||
_phantom: PhantomData<Io2>,
|
||||
}
|
||||
|
||||
impl<T, Io1, Io2> Future for InnerConnectorResponseA<T, Io1, Io2>
|
||||
where
|
||||
T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
@ -520,18 +508,16 @@ mod connect_impl {
|
||||
pub(crate) struct InnerConnectorResponseB<T, Io1, Io2>
|
||||
where
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
#[pin]
|
||||
fut: <ConnectionPool<T, Io2> as Service>::Future,
|
||||
_t: PhantomData<Io1>,
|
||||
fut: <ConnectionPool<T, Io2> as Service<Connect>>::Future,
|
||||
_phantom: PhantomData<Io1>,
|
||||
}
|
||||
|
||||
impl<T, Io1, Io2> Future for InnerConnectorResponseB<T, Io1, Io2>
|
||||
where
|
||||
T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
@ -545,3 +531,82 @@ mod connect_impl {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "trust-dns"))]
|
||||
mod resolver {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn resolver() -> Resolver {
|
||||
Resolver::Default
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "trust-dns")]
|
||||
mod resolver {
|
||||
use std::{cell::RefCell, net::SocketAddr};
|
||||
|
||||
use actix_tls::connect::Resolve;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use trust_dns_resolver::{
|
||||
config::{ResolverConfig, ResolverOpts},
|
||||
system_conf::read_system_conf,
|
||||
TokioAsyncResolver,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(super) fn resolver() -> Resolver {
|
||||
// new type for impl Resolve trait for TokioAsyncResolver.
|
||||
struct TrustDnsResolver(TokioAsyncResolver);
|
||||
|
||||
impl Resolve for TrustDnsResolver {
|
||||
fn lookup<'a>(
|
||||
&'a self,
|
||||
host: &'a str,
|
||||
port: u16,
|
||||
) -> LocalBoxFuture<'a, Result<Vec<SocketAddr>, Box<dyn std::error::Error>>>
|
||||
{
|
||||
Box::pin(async move {
|
||||
let res = self
|
||||
.0
|
||||
.lookup_ip(host)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|ip| SocketAddr::new(ip, port))
|
||||
.collect();
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// dns struct is cached in thread local.
|
||||
// so new client constructor can reuse the existing dns resolver.
|
||||
thread_local! {
|
||||
static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
// get from thread local or construct a new trust-dns resolver.
|
||||
TRUST_DNS_RESOLVER.with(|local| {
|
||||
let resolver = local.borrow().as_ref().map(Clone::clone);
|
||||
match resolver {
|
||||
Some(resolver) => resolver,
|
||||
None => {
|
||||
let (cfg, opts) = match read_system_conf() {
|
||||
Ok((cfg, opts)) => (cfg, opts),
|
||||
Err(e) => {
|
||||
log::error!("TRust-DNS can not load system config: {}", e);
|
||||
(ResolverConfig::default(), ResolverOpts::default())
|
||||
}
|
||||
};
|
||||
|
||||
let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap();
|
||||
|
||||
// box trust dns resolver and put it in thread local.
|
||||
let resolver = Resolver::new_custom(TrustDnsResolver(resolver));
|
||||
*local.borrow_mut() = Some(resolver.clone());
|
||||
resolver
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
use std::io;
|
||||
|
||||
use actix_connect::resolver::ResolveError;
|
||||
use derive_more::{Display, From};
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
use actix_connect::ssl::openssl::{HandshakeError, SslError};
|
||||
use actix_tls::accept::openssl::SslError;
|
||||
|
||||
use crate::error::{Error, ParseError, ResponseError};
|
||||
use crate::http::{Error as HttpError, StatusCode};
|
||||
@ -21,14 +20,9 @@ pub enum ConnectError {
|
||||
#[display(fmt = "{}", _0)]
|
||||
SslError(SslError),
|
||||
|
||||
/// SSL Handshake error
|
||||
#[cfg(feature = "openssl")]
|
||||
#[display(fmt = "{}", _0)]
|
||||
SslHandshakeError(String),
|
||||
|
||||
/// Failed to resolve the hostname
|
||||
#[display(fmt = "Failed resolving hostname: {}", _0)]
|
||||
Resolver(ResolveError),
|
||||
Resolver(Box<dyn std::error::Error>),
|
||||
|
||||
/// No dns records
|
||||
#[display(fmt = "No dns records found for the input")]
|
||||
@ -39,7 +33,7 @@ pub enum ConnectError {
|
||||
H2(h2::Error),
|
||||
|
||||
/// Connecting took too long
|
||||
#[display(fmt = "Timeout out while establishing connection")]
|
||||
#[display(fmt = "Timeout while establishing connection")]
|
||||
Timeout,
|
||||
|
||||
/// Connector has been disconnected
|
||||
@ -57,25 +51,18 @@ pub enum ConnectError {
|
||||
|
||||
impl std::error::Error for ConnectError {}
|
||||
|
||||
impl From<actix_connect::ConnectError> for ConnectError {
|
||||
fn from(err: actix_connect::ConnectError) -> ConnectError {
|
||||
impl From<actix_tls::connect::ConnectError> for ConnectError {
|
||||
fn from(err: actix_tls::connect::ConnectError) -> ConnectError {
|
||||
match err {
|
||||
actix_connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
|
||||
actix_connect::ConnectError::NoRecords => ConnectError::NoRecords,
|
||||
actix_connect::ConnectError::InvalidInput => panic!(),
|
||||
actix_connect::ConnectError::Unresolved => ConnectError::Unresolved,
|
||||
actix_connect::ConnectError::Io(e) => ConnectError::Io(e),
|
||||
actix_tls::connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
|
||||
actix_tls::connect::ConnectError::NoRecords => ConnectError::NoRecords,
|
||||
actix_tls::connect::ConnectError::InvalidInput => panic!(),
|
||||
actix_tls::connect::ConnectError::Unresolved => ConnectError::Unresolved,
|
||||
actix_tls::connect::ConnectError::Io(e) => ConnectError::Io(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
impl<T: std::fmt::Debug> From<HandshakeError<T>> for ConnectError {
|
||||
fn from(err: HandshakeError<T>) -> ConnectError {
|
||||
ConnectError::SslHandshakeError(format!("{:?}", err))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, From)]
|
||||
pub enum InvalidUrl {
|
||||
#[display(fmt = "Missing url scheme")]
|
||||
@ -110,7 +97,7 @@ pub enum SendRequestError {
|
||||
#[display(fmt = "{}", _0)]
|
||||
H2(h2::Error),
|
||||
/// Response took too long
|
||||
#[display(fmt = "Timeout out while waiting for response")]
|
||||
#[display(fmt = "Timeout while waiting for response")]
|
||||
Timeout,
|
||||
/// Tunnels are not supported for http2 connection
|
||||
#[display(fmt = "Tunnels are not supported for http2 connection")]
|
||||
|
@ -1,10 +1,10 @@
|
||||
use std::io::Write;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{io, mem, time};
|
||||
use std::{io, time};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use bytes::buf::BufMutExt;
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
|
||||
use bytes::buf::BufMut;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::Stream;
|
||||
use futures_util::future::poll_fn;
|
||||
@ -45,14 +45,14 @@ where
|
||||
Some(port) => write!(wrt, "{}:{}", host, port),
|
||||
};
|
||||
|
||||
match wrt.get_mut().split().freeze().try_into() {
|
||||
match wrt.get_mut().split().freeze().try_into_value() {
|
||||
Ok(value) => match head {
|
||||
RequestHeadType::Owned(ref mut head) => {
|
||||
head.headers.insert(HOST, value)
|
||||
head.headers.insert(HOST, value);
|
||||
}
|
||||
RequestHeadType::Rc(_, ref mut extra_headers) => {
|
||||
let headers = extra_headers.get_or_insert(HeaderMap::new());
|
||||
headers.insert(HOST, value)
|
||||
headers.insert(HOST, value);
|
||||
}
|
||||
},
|
||||
Err(e) => log::error!("Can not set HOST header {}", e),
|
||||
@ -67,17 +67,17 @@ where
|
||||
};
|
||||
|
||||
// create Framed and send request
|
||||
let mut framed = Framed::new(io, h1::ClientCodec::default());
|
||||
framed.send((head, body.size()).into()).await?;
|
||||
let mut framed_inner = Framed::new(io, h1::ClientCodec::default());
|
||||
framed_inner.send((head, body.size()).into()).await?;
|
||||
|
||||
// send request body
|
||||
match body.size() {
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0) => (),
|
||||
_ => send_body(body, &mut framed).await?,
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {}
|
||||
_ => send_body(body, Pin::new(&mut framed_inner)).await?,
|
||||
};
|
||||
|
||||
// read response and init read body
|
||||
let res = framed.into_future().await;
|
||||
let res = Pin::new(&mut framed_inner).into_future().await;
|
||||
let (head, framed) = if let (Some(result), framed) = res {
|
||||
let item = result.map_err(SendRequestError::from)?;
|
||||
(item, framed)
|
||||
@ -85,14 +85,14 @@ where
|
||||
return Err(SendRequestError::from(ConnectError::Disconnected));
|
||||
};
|
||||
|
||||
match framed.get_codec().message_type() {
|
||||
match framed.codec_ref().message_type() {
|
||||
h1::MessageType::None => {
|
||||
let force_close = !framed.get_codec().keepalive();
|
||||
let force_close = !framed.codec_ref().keepalive();
|
||||
release_connection(framed, force_close);
|
||||
Ok((head, Payload::None))
|
||||
}
|
||||
_ => {
|
||||
let pl: PayloadStream = PlStream::new(framed).boxed_local();
|
||||
let pl: PayloadStream = PlStream::new(framed_inner).boxed_local();
|
||||
Ok((head, pl.into()))
|
||||
}
|
||||
}
|
||||
@ -119,35 +119,36 @@ where
|
||||
}
|
||||
|
||||
/// send request body to the peer
|
||||
pub(crate) async fn send_body<I, B>(
|
||||
pub(crate) async fn send_body<T, B>(
|
||||
body: B,
|
||||
framed: &mut Framed<I, h1::ClientCodec>,
|
||||
mut framed: Pin<&mut Framed<T, h1::ClientCodec>>,
|
||||
) -> Result<(), SendRequestError>
|
||||
where
|
||||
I: ConnectionLifetime,
|
||||
T: ConnectionLifetime + Unpin,
|
||||
B: MessageBody,
|
||||
{
|
||||
let mut eof = false;
|
||||
pin_mut!(body);
|
||||
|
||||
let mut eof = false;
|
||||
while !eof {
|
||||
while !eof && !framed.is_write_buf_full() {
|
||||
while !eof && !framed.as_ref().is_write_buf_full() {
|
||||
match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
||||
Some(result) => {
|
||||
framed.write(h1::Message::Chunk(Some(result?)))?;
|
||||
framed.as_mut().write(h1::Message::Chunk(Some(result?)))?;
|
||||
}
|
||||
None => {
|
||||
eof = true;
|
||||
framed.write(h1::Message::Chunk(None))?;
|
||||
framed.as_mut().write(h1::Message::Chunk(None))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !framed.is_write_buf_empty() {
|
||||
poll_fn(|cx| match framed.flush(cx) {
|
||||
if !framed.as_ref().is_write_buf_empty() {
|
||||
poll_fn(|cx| match framed.as_mut().flush(cx) {
|
||||
Poll::Ready(Ok(_)) => Poll::Ready(Ok(())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => {
|
||||
if !framed.is_write_buf_full() {
|
||||
if !framed.as_ref().is_write_buf_full() {
|
||||
Poll::Ready(Ok(()))
|
||||
} else {
|
||||
Poll::Pending
|
||||
@ -158,13 +159,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
SinkExt::flush(framed).await?;
|
||||
SinkExt::flush(Pin::into_inner(framed)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// HTTP client connection
|
||||
pub struct H1Connection<T> {
|
||||
/// T should be `Unpin`
|
||||
io: Option<T>,
|
||||
created: time::Instant,
|
||||
pool: Option<Acquired<T>>,
|
||||
@ -175,7 +177,7 @@ where
|
||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
/// Close connection
|
||||
fn close(&mut self) {
|
||||
fn close(mut self: Pin<&mut Self>) {
|
||||
if let Some(mut pool) = self.pool.take() {
|
||||
if let Some(io) = self.io.take() {
|
||||
pool.close(IoConnection::new(
|
||||
@ -188,7 +190,7 @@ where
|
||||
}
|
||||
|
||||
/// Release this connection to the connection pool
|
||||
fn release(&mut self) {
|
||||
fn release(mut self: Pin<&mut Self>) {
|
||||
if let Some(mut pool) = self.pool.take() {
|
||||
if let Some(io) = self.io.take() {
|
||||
pool.release(IoConnection::new(
|
||||
@ -202,18 +204,11 @@ where
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncRead for H1Connection<T> {
|
||||
unsafe fn prepare_uninitialized_buffer(
|
||||
&self,
|
||||
buf: &mut [mem::MaybeUninit<u8>],
|
||||
) -> bool {
|
||||
self.io.as_ref().unwrap().prepare_uninitialized_buffer(buf)
|
||||
}
|
||||
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
@ -242,14 +237,18 @@ impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncWrite for H1Connection<T>
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pub(crate) struct PlStream<Io> {
|
||||
#[pin]
|
||||
framed: Option<Framed<Io, h1::ClientPayloadCodec>>,
|
||||
}
|
||||
|
||||
impl<Io: ConnectionLifetime> PlStream<Io> {
|
||||
fn new(framed: Framed<Io, h1::ClientCodec>) -> Self {
|
||||
let framed = framed.into_map_codec(|codec| codec.into_payload_codec());
|
||||
|
||||
PlStream {
|
||||
framed: Some(framed.map_codec(|codec| codec.into_payload_codec())),
|
||||
framed: Some(framed),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -261,16 +260,16 @@ impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
let mut this = self.project();
|
||||
|
||||
match this.framed.as_mut().unwrap().next_item(cx)? {
|
||||
match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Some(chunk)) => {
|
||||
if let Some(chunk) = chunk {
|
||||
Poll::Ready(Some(Ok(chunk)))
|
||||
} else {
|
||||
let framed = this.framed.take().unwrap();
|
||||
let force_close = !framed.get_codec().keepalive();
|
||||
let framed = this.framed.as_mut().as_pin_mut().unwrap();
|
||||
let force_close = !framed.codec_ref().keepalive();
|
||||
release_connection(framed, force_close);
|
||||
Poll::Ready(None)
|
||||
}
|
||||
@ -280,14 +279,13 @@ impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
|
||||
}
|
||||
}
|
||||
|
||||
fn release_connection<T, U>(framed: Framed<T, U>, force_close: bool)
|
||||
fn release_connection<T, U>(framed: Pin<&mut Framed<T, U>>, force_close: bool)
|
||||
where
|
||||
T: ConnectionLifetime,
|
||||
{
|
||||
let mut parts = framed.into_parts();
|
||||
if !force_close && parts.read_buf.is_empty() && parts.write_buf.is_empty() {
|
||||
parts.io.release()
|
||||
if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() {
|
||||
framed.io_pin().release()
|
||||
} else {
|
||||
parts.io.close()
|
||||
framed.io_pin().close()
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,10 @@ use super::config::ConnectorConfig;
|
||||
use super::connection::{ConnectionType, IoConnection};
|
||||
use super::error::SendRequestError;
|
||||
use super::pool::Acquired;
|
||||
use crate::client::connection::H2Connection;
|
||||
|
||||
pub(crate) async fn send_request<T, B>(
|
||||
mut io: SendRequest<Bytes>,
|
||||
mut io: H2Connection,
|
||||
head: RequestHeadType,
|
||||
body: B,
|
||||
created: time::Instant,
|
||||
@ -37,10 +38,10 @@ where
|
||||
trace!("Sending client request: {:?} {:?}", head, body.size());
|
||||
let head_req = head.as_ref().method == Method::HEAD;
|
||||
let length = body.size();
|
||||
let eof = match length {
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0) => true,
|
||||
_ => false,
|
||||
};
|
||||
let eof = matches!(
|
||||
length,
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0)
|
||||
);
|
||||
|
||||
let mut req = Request::new(());
|
||||
*req.uri_mut() = head.as_ref().uri.clone();
|
||||
@ -89,7 +90,7 @@ where
|
||||
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
|
||||
CONTENT_LENGTH if skip_len => continue,
|
||||
// DATE => has_date = true,
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
req.headers_mut().append(key, value.clone());
|
||||
}
|
||||
@ -171,9 +172,9 @@ async fn send_body<B: MessageBody>(
|
||||
}
|
||||
}
|
||||
|
||||
// release SendRequest object
|
||||
/// release SendRequest object
|
||||
fn release<T: AsyncRead + AsyncWrite + Unpin + 'static>(
|
||||
io: SendRequest<Bytes>,
|
||||
io: H2Connection,
|
||||
pool: Option<Acquired<T>>,
|
||||
created: time::Instant,
|
||||
close: bool,
|
||||
|
@ -6,13 +6,15 @@ use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::time::{delay_for, Delay};
|
||||
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use actix_rt::time::{sleep, Sleep};
|
||||
use actix_service::Service;
|
||||
use actix_utils::{oneshot, task::LocalWaker};
|
||||
use actix_utils::task::LocalWaker;
|
||||
use ahash::AHashMap;
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::{poll_fn, FutureExt, LocalBoxFuture};
|
||||
use fxhash::FxHashMap;
|
||||
use futures_channel::oneshot;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures_util::future::{poll_fn, FutureExt};
|
||||
use h2::client::{Connection, SendRequest};
|
||||
use http::uri::Authority;
|
||||
use indexmap::IndexSet;
|
||||
@ -24,6 +26,7 @@ use super::connection::{ConnectionType, IoConnection};
|
||||
use super::error::ConnectError;
|
||||
use super::h2proto::handshake;
|
||||
use super::Connect;
|
||||
use crate::client::connection::H2Connection;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
/// Protocol version
|
||||
@ -44,26 +47,31 @@ impl From<Authority> for Key {
|
||||
}
|
||||
|
||||
/// Connections pool
|
||||
pub(crate) struct ConnectionPool<T, Io: 'static>(Rc<RefCell<T>>, Rc<RefCell<Inner<Io>>>);
|
||||
pub(crate) struct ConnectionPool<T, Io: 'static>(Rc<T>, Rc<RefCell<Inner<Io>>>);
|
||||
|
||||
impl<T, Io> ConnectionPool<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
pub(crate) fn new(connector: T, config: ConnectorConfig) -> Self {
|
||||
ConnectionPool(
|
||||
Rc::new(RefCell::new(connector)),
|
||||
Rc::new(RefCell::new(Inner {
|
||||
config,
|
||||
acquired: 0,
|
||||
waiters: Slab::new(),
|
||||
waiters_queue: IndexSet::new(),
|
||||
available: FxHashMap::default(),
|
||||
waker: LocalWaker::new(),
|
||||
})),
|
||||
)
|
||||
let connector_rc = Rc::new(connector);
|
||||
let inner_rc = Rc::new(RefCell::new(Inner {
|
||||
config,
|
||||
acquired: 0,
|
||||
waiters: Slab::new(),
|
||||
waiters_queue: IndexSet::new(),
|
||||
available: AHashMap::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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,29 +84,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Io> Service for ConnectionPool<T, Io>
|
||||
impl<T, Io> Drop for ConnectionPool<T, Io> {
|
||||
fn drop(&mut self) {
|
||||
// wake up the ConnectorPoolSupport when dropping so it can exit properly.
|
||||
self.1.borrow().waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Io> Service<Connect> for ConnectionPool<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
|
||||
+ 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
type Request = Connect;
|
||||
type Response = IoConnection<Io>;
|
||||
type Error = ConnectError;
|
||||
type Future = LocalBoxFuture<'static, Result<IoConnection<Io>, ConnectError>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.0.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Connect) -> Self::Future {
|
||||
// start support future
|
||||
actix_rt::spawn(ConnectorPoolSupport {
|
||||
connector: self.0.clone(),
|
||||
inner: self.1.clone(),
|
||||
});
|
||||
|
||||
let mut connector = self.0.clone();
|
||||
fn call(&self, req: Connect) -> Self::Future {
|
||||
let connector = self.0.clone();
|
||||
let inner = self.1.clone();
|
||||
|
||||
let fut = async move {
|
||||
@ -112,11 +119,11 @@ where
|
||||
match poll_fn(|cx| Poll::Ready(inner.borrow_mut().acquire(&key, cx))).await {
|
||||
Acquire::Acquired(io, created) => {
|
||||
// use existing connection
|
||||
return Ok(IoConnection::new(
|
||||
Ok(IoConnection::new(
|
||||
io,
|
||||
created,
|
||||
Some(Acquired(key, Some(inner))),
|
||||
));
|
||||
))
|
||||
}
|
||||
Acquire::Available => {
|
||||
// open tcp connection
|
||||
@ -133,10 +140,9 @@ where
|
||||
Some(guard.consume()),
|
||||
))
|
||||
} else {
|
||||
let (snd, connection) = handshake(io, &config).await?;
|
||||
actix_rt::spawn(connection.map(|_| ()));
|
||||
let (sender, connection) = handshake(io, &config).await?;
|
||||
Ok(IoConnection::new(
|
||||
ConnectionType::H2(snd),
|
||||
ConnectionType::H2(H2Connection::new(sender, connection)),
|
||||
Instant::now(),
|
||||
Some(guard.consume()),
|
||||
))
|
||||
@ -252,7 +258,7 @@ struct AvailableConnection<Io> {
|
||||
pub(crate) struct Inner<Io> {
|
||||
config: ConnectorConfig,
|
||||
acquired: usize,
|
||||
available: FxHashMap<Key, VecDeque<AvailableConnection<Io>>>,
|
||||
available: AHashMap<Key, VecDeque<AvailableConnection<Io>>>,
|
||||
waiters: Slab<
|
||||
Option<(
|
||||
Connect,
|
||||
@ -320,21 +326,22 @@ where
|
||||
{
|
||||
if let Some(timeout) = self.config.disconnect_timeout {
|
||||
if let ConnectionType::H1(io) = conn.io {
|
||||
actix_rt::spawn(CloseConnection::new(io, timeout))
|
||||
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 buf) {
|
||||
Poll::Pending => (),
|
||||
Poll::Ready(Ok(n)) if n > 0 => {
|
||||
match Pin::new(s).poll_read(cx, &mut read_buf) {
|
||||
Poll::Pending => {}
|
||||
Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => {
|
||||
if let Some(timeout) = self.config.disconnect_timeout {
|
||||
if let ConnectionType::H1(io) = io {
|
||||
actix_rt::spawn(CloseConnection::new(
|
||||
io, timeout,
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@ -366,7 +373,7 @@ where
|
||||
self.acquired -= 1;
|
||||
if let Some(timeout) = self.config.disconnect_timeout {
|
||||
if let ConnectionType::H1(io) = io {
|
||||
actix_rt::spawn(CloseConnection::new(io, timeout))
|
||||
actix_rt::spawn(CloseConnection::new(io, timeout));
|
||||
}
|
||||
}
|
||||
self.check_availability();
|
||||
@ -379,9 +386,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
struct CloseConnection<T> {
|
||||
io: T,
|
||||
timeout: Delay,
|
||||
#[pin]
|
||||
timeout: Sleep,
|
||||
}
|
||||
|
||||
impl<T> CloseConnection<T>
|
||||
@ -391,7 +400,7 @@ where
|
||||
fn new(io: T, timeout: Duration) -> Self {
|
||||
CloseConnection {
|
||||
io,
|
||||
timeout: delay_for(timeout),
|
||||
timeout: sleep(timeout),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -403,11 +412,11 @@ where
|
||||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
||||
let this = self.get_mut();
|
||||
let this = self.project();
|
||||
|
||||
match Pin::new(&mut this.timeout).poll(cx) {
|
||||
match this.timeout.poll(cx) {
|
||||
Poll::Ready(_) => Poll::Ready(()),
|
||||
Poll::Pending => match Pin::new(&mut this.io).poll_shutdown(cx) {
|
||||
Poll::Pending => match Pin::new(this.io).poll_shutdown(cx) {
|
||||
Poll::Ready(_) => Poll::Ready(()),
|
||||
Poll::Pending => Poll::Pending,
|
||||
},
|
||||
@ -420,14 +429,14 @@ struct ConnectorPoolSupport<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
connector: T,
|
||||
connector: Rc<T>,
|
||||
inner: Rc<RefCell<Inner<Io>>>,
|
||||
}
|
||||
|
||||
impl<T, Io> Future for ConnectorPoolSupport<T, Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError>,
|
||||
T::Future: 'static,
|
||||
{
|
||||
type Output = ();
|
||||
@ -435,7 +444,13 @@ where
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
|
||||
let mut inner = this.inner.as_ref().borrow_mut();
|
||||
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
|
||||
@ -521,7 +536,7 @@ where
|
||||
rx: Some(rx),
|
||||
inner: Some(inner),
|
||||
config,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -551,11 +566,10 @@ where
|
||||
|
||||
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(|_| ()));
|
||||
Poll::Ready(Ok((sender, connection))) => {
|
||||
let rx = this.rx.take().unwrap();
|
||||
let _ = rx.send(Ok(IoConnection::new(
|
||||
ConnectionType::H2(snd),
|
||||
ConnectionType::H2(H2Connection::new(sender, connection)),
|
||||
Instant::now(),
|
||||
Some(Acquired(this.key.clone(), this.inner.take())),
|
||||
)));
|
||||
|
0
actix-http/src/clinu/mod.rs
Normal file
0
actix-http/src/clinu/mod.rs
Normal file
@ -1,40 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_service::Service;
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Service that allows to turn non-clone service to a service with `Clone` impl
|
||||
///
|
||||
/// # Panics
|
||||
/// CloneableService might panic with some creative use of thread local storage.
|
||||
/// See https://github.com/actix/actix-web/issues/1295 for example
|
||||
pub(crate) struct CloneableService<T: Service>(Rc<RefCell<T>>);
|
||||
|
||||
impl<T: Service> CloneableService<T> {
|
||||
pub(crate) fn new(service: T) -> Self {
|
||||
Self(Rc::new(RefCell::new(service)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Service> Clone for CloneableService<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Service> Service for CloneableService<T> {
|
||||
type Request = T::Request;
|
||||
type Response = T::Response;
|
||||
type Error = T::Error;
|
||||
type Future = T::Future;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.0.borrow_mut().poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: T::Request) -> Self::Future {
|
||||
self.0.borrow_mut().call(req)
|
||||
}
|
||||
}
|
@ -4,12 +4,12 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use std::{fmt, net};
|
||||
|
||||
use actix_rt::time::{delay_for, delay_until, Delay, Instant};
|
||||
use actix_rt::time::{sleep, sleep_until, Instant, Sleep};
|
||||
use bytes::BytesMut;
|
||||
use futures_util::{future, FutureExt};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||
/// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||
const DATE_VALUE_LENGTH: usize = 29;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
@ -17,7 +17,7 @@ const DATE_VALUE_LENGTH: usize = 29;
|
||||
pub enum KeepAlive {
|
||||
/// Keep alive in seconds
|
||||
Timeout(usize),
|
||||
/// Relay on OS to shutdown tcp connection
|
||||
/// Rely on OS to shutdown tcp connection
|
||||
Os,
|
||||
/// Disabled
|
||||
Disabled,
|
||||
@ -121,10 +121,10 @@ impl ServiceConfig {
|
||||
|
||||
#[inline]
|
||||
/// Client timeout for first request.
|
||||
pub fn client_timer(&self) -> Option<Delay> {
|
||||
pub fn client_timer(&self) -> Option<Sleep> {
|
||||
let delay_time = self.0.client_timeout;
|
||||
if delay_time != 0 {
|
||||
Some(delay_until(
|
||||
Some(sleep_until(
|
||||
self.0.timer.now() + Duration::from_millis(delay_time),
|
||||
))
|
||||
} else {
|
||||
@ -154,9 +154,9 @@ impl ServiceConfig {
|
||||
|
||||
#[inline]
|
||||
/// Return keep-alive timer delay is configured.
|
||||
pub fn keep_alive_timer(&self) -> Option<Delay> {
|
||||
pub fn keep_alive_timer(&self) -> Option<Sleep> {
|
||||
if let Some(ka) = self.0.keep_alive {
|
||||
Some(delay_until(self.0.timer.now() + ka))
|
||||
Some(sleep_until(self.0.timer.now() + ka))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -209,6 +209,7 @@ impl Date {
|
||||
date.update();
|
||||
date
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.pos = 0;
|
||||
write!(
|
||||
@ -265,7 +266,7 @@ impl DateService {
|
||||
|
||||
// periodic date update
|
||||
let s = self.clone();
|
||||
actix_rt::spawn(delay_for(Duration::from_millis(500)).then(move |_| {
|
||||
actix_rt::spawn(sleep(Duration::from_millis(500)).then(move |_| {
|
||||
s.0.reset();
|
||||
future::ready(())
|
||||
}));
|
||||
|
@ -1,252 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
use super::{Cookie, SameSite};
|
||||
|
||||
/// Structure that follows the builder pattern for building `Cookie` structs.
|
||||
///
|
||||
/// To construct a cookie:
|
||||
///
|
||||
/// 1. Call [`Cookie::build`](struct.Cookie.html#method.build) to start building.
|
||||
/// 2. Use any of the builder methods to set fields in the cookie.
|
||||
/// 3. Call [finish](#method.finish) to retrieve the built cookie.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let cookie: Cookie = Cookie::build("name", "value")
|
||||
/// .domain("www.rust-lang.org")
|
||||
/// .path("/")
|
||||
/// .secure(true)
|
||||
/// .http_only(true)
|
||||
/// .max_age(84600)
|
||||
/// .finish();
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CookieBuilder {
|
||||
/// The cookie being built.
|
||||
cookie: Cookie<'static>,
|
||||
}
|
||||
|
||||
impl CookieBuilder {
|
||||
/// Creates a new `CookieBuilder` instance from the given name and value.
|
||||
///
|
||||
/// This method is typically called indirectly via
|
||||
/// [Cookie::build](struct.Cookie.html#method.build).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar").finish();
|
||||
/// assert_eq!(c.name_value(), ("foo", "bar"));
|
||||
/// ```
|
||||
pub fn new<N, V>(name: N, value: V) -> CookieBuilder
|
||||
where
|
||||
N: Into<Cow<'static, str>>,
|
||||
V: Into<Cow<'static, str>>,
|
||||
{
|
||||
CookieBuilder {
|
||||
cookie: Cookie::new(name, value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the `expires` field in the cookie being built.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .expires(time::OffsetDateTime::now_utc())
|
||||
/// .finish();
|
||||
///
|
||||
/// assert!(c.expires().is_some());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn expires(mut self, when: OffsetDateTime) -> CookieBuilder {
|
||||
self.cookie.set_expires(when);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `max_age` field in seconds in the cookie being built.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .max_age(1800)
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60)));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn max_age(self, seconds: i64) -> CookieBuilder {
|
||||
self.max_age_time(Duration::seconds(seconds))
|
||||
}
|
||||
|
||||
/// Sets the `max_age` field in the cookie being built.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .max_age_time(time::Duration::minutes(30))
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60)));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn max_age_time(mut self, value: Duration) -> CookieBuilder {
|
||||
// Truncate any nanoseconds from the Duration, as they aren't represented within `Max-Age`
|
||||
// and would cause two otherwise identical `Cookie` instances to not be equivalent to one another.
|
||||
self.cookie
|
||||
.set_max_age(Duration::seconds(value.whole_seconds()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `domain` field in the cookie being built.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .domain("www.rust-lang.org")
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.domain(), Some("www.rust-lang.org"));
|
||||
/// ```
|
||||
pub fn domain<D: Into<Cow<'static, str>>>(mut self, value: D) -> CookieBuilder {
|
||||
self.cookie.set_domain(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `path` field in the cookie being built.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .path("/")
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.path(), Some("/"));
|
||||
/// ```
|
||||
pub fn path<P: Into<Cow<'static, str>>>(mut self, path: P) -> CookieBuilder {
|
||||
self.cookie.set_path(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `secure` field in the cookie being built.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .secure(true)
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.secure(), Some(true));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn secure(mut self, value: bool) -> CookieBuilder {
|
||||
self.cookie.set_secure(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `http_only` field in the cookie being built.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .http_only(true)
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.http_only(), Some(true));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn http_only(mut self, value: bool) -> CookieBuilder {
|
||||
self.cookie.set_http_only(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `same_site` field in the cookie being built.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{Cookie, SameSite};
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .same_site(SameSite::Strict)
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.same_site(), Some(SameSite::Strict));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn same_site(mut self, value: SameSite) -> CookieBuilder {
|
||||
self.cookie.set_same_site(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Makes the cookie being built 'permanent' by extending its expiration and
|
||||
/// max age 20 years into the future.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
/// use time::Duration;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .permanent()
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.max_age(), Some(Duration::days(365 * 20)));
|
||||
/// # assert!(c.expires().is_some());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn permanent(mut self) -> CookieBuilder {
|
||||
self.cookie.make_permanent();
|
||||
self
|
||||
}
|
||||
|
||||
/// Finishes building and returns the built `Cookie`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Cookie;
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .domain("crates.io")
|
||||
/// .path("/")
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.name_value(), ("foo", "bar"));
|
||||
/// assert_eq!(c.domain(), Some("crates.io"));
|
||||
/// assert_eq!(c.path(), Some("/"));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn finish(self) -> Cookie<'static> {
|
||||
self.cookie
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use super::Cookie;
|
||||
|
||||
/// A `DeltaCookie` is a helper structure used in a cookie jar. It wraps a
|
||||
/// `Cookie` so that it can be hashed and compared purely by name. It further
|
||||
/// records whether the wrapped cookie is a "removal" cookie, that is, a cookie
|
||||
/// that when sent to the client removes the named cookie on the client's
|
||||
/// machine.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeltaCookie {
|
||||
pub cookie: Cookie<'static>,
|
||||
pub removed: bool,
|
||||
}
|
||||
|
||||
impl DeltaCookie {
|
||||
/// Create a new `DeltaCookie` that is being added to a jar.
|
||||
#[inline]
|
||||
pub fn added(cookie: Cookie<'static>) -> DeltaCookie {
|
||||
DeltaCookie {
|
||||
cookie,
|
||||
removed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `DeltaCookie` that is being removed from a jar. The
|
||||
/// `cookie` should be a "removal" cookie.
|
||||
#[inline]
|
||||
pub fn removed(cookie: Cookie<'static>) -> DeltaCookie {
|
||||
DeltaCookie {
|
||||
cookie,
|
||||
removed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DeltaCookie {
|
||||
type Target = Cookie<'static>;
|
||||
|
||||
fn deref(&self) -> &Cookie<'static> {
|
||||
&self.cookie
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for DeltaCookie {
|
||||
fn deref_mut(&mut self) -> &mut Cookie<'static> {
|
||||
&mut self.cookie
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DeltaCookie {
|
||||
fn eq(&self, other: &DeltaCookie) -> bool {
|
||||
self.name() == other.name()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for DeltaCookie {}
|
||||
|
||||
impl Hash for DeltaCookie {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.name().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<str> for DeltaCookie {
|
||||
fn borrow(&self) -> &str {
|
||||
self.name()
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
//! This module contains types that represent cookie properties that are not yet
|
||||
//! standardized. That is, _draft_ features.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// The `SameSite` cookie attribute.
|
||||
///
|
||||
/// A cookie with a `SameSite` attribute is imposed restrictions on when it is
|
||||
/// sent to the origin server in a cross-site request. If the `SameSite`
|
||||
/// attribute is "Strict", then the cookie is never sent in cross-site requests.
|
||||
/// If the `SameSite` attribute is "Lax", the cookie is only sent in cross-site
|
||||
/// requests with "safe" HTTP methods, i.e, `GET`, `HEAD`, `OPTIONS`, `TRACE`.
|
||||
/// If the `SameSite` attribute is not present then the cookie will be sent as
|
||||
/// normal. In some browsers, this will implicitly handle the cookie as if "Lax"
|
||||
/// and in others, "None". It's best to explicitly set the `SameSite` attribute
|
||||
/// to avoid inconsistent behavior.
|
||||
///
|
||||
/// **Note:** Depending on browser, the `Secure` attribute may be required for
|
||||
/// `SameSite` "None" cookies to be accepted.
|
||||
///
|
||||
/// **Note:** This cookie attribute is an HTTP draft! Its meaning and definition
|
||||
/// are subject to change.
|
||||
///
|
||||
/// More info about these draft changes can be found in the draft spec:
|
||||
/// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SameSite {
|
||||
/// The "Strict" `SameSite` attribute.
|
||||
Strict,
|
||||
/// The "Lax" `SameSite` attribute.
|
||||
Lax,
|
||||
/// The "None" `SameSite` attribute.
|
||||
None,
|
||||
}
|
||||
|
||||
impl SameSite {
|
||||
/// Returns `true` if `self` is `SameSite::Strict` and `false` otherwise.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::SameSite;
|
||||
///
|
||||
/// let strict = SameSite::Strict;
|
||||
/// assert!(strict.is_strict());
|
||||
/// assert!(!strict.is_lax());
|
||||
/// assert!(!strict.is_none());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn is_strict(self) -> bool {
|
||||
match self {
|
||||
SameSite::Strict => true,
|
||||
SameSite::Lax | SameSite::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is `SameSite::Lax` and `false` otherwise.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::SameSite;
|
||||
///
|
||||
/// let lax = SameSite::Lax;
|
||||
/// assert!(lax.is_lax());
|
||||
/// assert!(!lax.is_strict());
|
||||
/// assert!(!lax.is_none());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn is_lax(self) -> bool {
|
||||
match self {
|
||||
SameSite::Lax => true,
|
||||
SameSite::Strict | SameSite::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is `SameSite::None` and `false` otherwise.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::SameSite;
|
||||
///
|
||||
/// let none = SameSite::None;
|
||||
/// assert!(none.is_none());
|
||||
/// assert!(!none.is_lax());
|
||||
/// assert!(!none.is_strict());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn is_none(self) -> bool {
|
||||
match self {
|
||||
SameSite::None => true,
|
||||
SameSite::Lax | SameSite::Strict => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SameSite {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
SameSite::Strict => write!(f, "Strict"),
|
||||
SameSite::Lax => write!(f, "Lax"),
|
||||
SameSite::None => write!(f, "None"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,651 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::mem;
|
||||
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
use super::delta::DeltaCookie;
|
||||
use super::Cookie;
|
||||
|
||||
#[cfg(feature = "secure-cookies")]
|
||||
use super::secure::{Key, PrivateJar, SignedJar};
|
||||
|
||||
/// A collection of cookies that tracks its modifications.
|
||||
///
|
||||
/// A `CookieJar` provides storage for any number of cookies. Any changes made
|
||||
/// to the jar are tracked; the changes can be retrieved via the
|
||||
/// [delta](#method.delta) method which returns an iterator over the changes.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// A jar's life begins via [new](#method.new) and calls to
|
||||
/// [`add_original`](#method.add_original):
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{Cookie, CookieJar};
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.add_original(Cookie::new("name", "value"));
|
||||
/// jar.add_original(Cookie::new("second", "another"));
|
||||
/// ```
|
||||
///
|
||||
/// Cookies can be added via [add](#method.add) and removed via
|
||||
/// [remove](#method.remove). Finally, cookies can be looked up via
|
||||
/// [get](#method.get):
|
||||
///
|
||||
/// ```rust
|
||||
/// # use actix_http::cookie::{Cookie, CookieJar};
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.add(Cookie::new("a", "one"));
|
||||
/// jar.add(Cookie::new("b", "two"));
|
||||
///
|
||||
/// assert_eq!(jar.get("a").map(|c| c.value()), Some("one"));
|
||||
/// assert_eq!(jar.get("b").map(|c| c.value()), Some("two"));
|
||||
///
|
||||
/// jar.remove(Cookie::named("b"));
|
||||
/// assert!(jar.get("b").is_none());
|
||||
/// ```
|
||||
///
|
||||
/// # Deltas
|
||||
///
|
||||
/// A jar keeps track of any modifications made to it over time. The
|
||||
/// modifications are recorded as cookies. The modifications can be retrieved
|
||||
/// via [delta](#method.delta). Any new `Cookie` added to a jar via `add`
|
||||
/// results in the same `Cookie` appearing in the `delta`; cookies added via
|
||||
/// `add_original` do not count towards the delta. Any _original_ cookie that is
|
||||
/// removed from a jar results in a "removal" cookie appearing in the delta. A
|
||||
/// "removal" cookie is a cookie that a server sends so that the cookie is
|
||||
/// removed from the client's machine.
|
||||
///
|
||||
/// Deltas are typically used to create `Set-Cookie` headers corresponding to
|
||||
/// the changes made to a cookie jar over a period of time.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use actix_http::cookie::{Cookie, CookieJar};
|
||||
/// let mut jar = CookieJar::new();
|
||||
///
|
||||
/// // original cookies don't affect the delta
|
||||
/// jar.add_original(Cookie::new("original", "value"));
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
///
|
||||
/// // new cookies result in an equivalent `Cookie` in the delta
|
||||
/// jar.add(Cookie::new("a", "one"));
|
||||
/// jar.add(Cookie::new("b", "two"));
|
||||
/// assert_eq!(jar.delta().count(), 2);
|
||||
///
|
||||
/// // removing an original cookie adds a "removal" cookie to the delta
|
||||
/// jar.remove(Cookie::named("original"));
|
||||
/// assert_eq!(jar.delta().count(), 3);
|
||||
///
|
||||
/// // removing a new cookie that was added removes that `Cookie` from the delta
|
||||
/// jar.remove(Cookie::named("a"));
|
||||
/// assert_eq!(jar.delta().count(), 2);
|
||||
/// ```
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct CookieJar {
|
||||
original_cookies: HashSet<DeltaCookie>,
|
||||
delta_cookies: HashSet<DeltaCookie>,
|
||||
}
|
||||
|
||||
impl CookieJar {
|
||||
/// Creates an empty cookie jar.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::CookieJar;
|
||||
///
|
||||
/// let jar = CookieJar::new();
|
||||
/// assert_eq!(jar.iter().count(), 0);
|
||||
/// ```
|
||||
pub fn new() -> CookieJar {
|
||||
CookieJar::default()
|
||||
}
|
||||
|
||||
/// Returns a reference to the `Cookie` inside this jar with the name
|
||||
/// `name`. If no such cookie exists, returns `None`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie};
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// assert!(jar.get("name").is_none());
|
||||
///
|
||||
/// jar.add(Cookie::new("name", "value"));
|
||||
/// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Option<&Cookie<'static>> {
|
||||
self.delta_cookies
|
||||
.get(name)
|
||||
.or_else(|| self.original_cookies.get(name))
|
||||
.and_then(|c| if !c.removed { Some(&c.cookie) } else { None })
|
||||
}
|
||||
|
||||
/// Adds an "original" `cookie` to this jar. If an original cookie with the
|
||||
/// same name already exists, it is replaced with `cookie`. Cookies added
|
||||
/// with `add` take precedence and are not replaced by this method.
|
||||
///
|
||||
/// Adding an original cookie does not affect the [delta](#method.delta)
|
||||
/// computation. This method is intended to be used to seed the cookie jar
|
||||
/// with cookies received from a client's HTTP message.
|
||||
///
|
||||
/// For accurate `delta` computations, this method should not be called
|
||||
/// after calling `remove`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie};
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.add_original(Cookie::new("name", "value"));
|
||||
/// jar.add_original(Cookie::new("second", "two"));
|
||||
///
|
||||
/// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
|
||||
/// assert_eq!(jar.get("second").map(|c| c.value()), Some("two"));
|
||||
/// assert_eq!(jar.iter().count(), 2);
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
/// ```
|
||||
pub fn add_original(&mut self, cookie: Cookie<'static>) {
|
||||
self.original_cookies.replace(DeltaCookie::added(cookie));
|
||||
}
|
||||
|
||||
/// Adds `cookie` to this jar. If a cookie with the same name already
|
||||
/// exists, it is replaced with `cookie`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie};
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.add(Cookie::new("name", "value"));
|
||||
/// jar.add(Cookie::new("second", "two"));
|
||||
///
|
||||
/// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
|
||||
/// assert_eq!(jar.get("second").map(|c| c.value()), Some("two"));
|
||||
/// assert_eq!(jar.iter().count(), 2);
|
||||
/// assert_eq!(jar.delta().count(), 2);
|
||||
/// ```
|
||||
pub fn add(&mut self, cookie: Cookie<'static>) {
|
||||
self.delta_cookies.replace(DeltaCookie::added(cookie));
|
||||
}
|
||||
|
||||
/// Removes `cookie` from this jar. If an _original_ cookie with the same
|
||||
/// name as `cookie` is present in the jar, a _removal_ cookie will be
|
||||
/// present in the `delta` computation. To properly generate the removal
|
||||
/// cookie, `cookie` must contain the same `path` and `domain` as the cookie
|
||||
/// that was initially set.
|
||||
///
|
||||
/// A "removal" cookie is a cookie that has the same name as the original
|
||||
/// cookie but has an empty value, a max-age of 0, and an expiration date
|
||||
/// far in the past.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Removing an _original_ cookie results in a _removal_ cookie:
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie};
|
||||
/// use time::Duration;
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
///
|
||||
/// // Assume this cookie originally had a path of "/" and domain of "a.b".
|
||||
/// jar.add_original(Cookie::new("name", "value"));
|
||||
///
|
||||
/// // If the path and domain were set, they must be provided to `remove`.
|
||||
/// jar.remove(Cookie::build("name", "").path("/").domain("a.b").finish());
|
||||
///
|
||||
/// // The delta will contain the removal cookie.
|
||||
/// let delta: Vec<_> = jar.delta().collect();
|
||||
/// assert_eq!(delta.len(), 1);
|
||||
/// assert_eq!(delta[0].name(), "name");
|
||||
/// assert_eq!(delta[0].max_age(), Some(Duration::zero()));
|
||||
/// ```
|
||||
///
|
||||
/// Removing a new cookie does not result in a _removal_ cookie:
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie};
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.add(Cookie::new("name", "value"));
|
||||
/// assert_eq!(jar.delta().count(), 1);
|
||||
///
|
||||
/// jar.remove(Cookie::named("name"));
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
/// ```
|
||||
pub fn remove(&mut self, mut cookie: Cookie<'static>) {
|
||||
if self.original_cookies.contains(cookie.name()) {
|
||||
cookie.set_value("");
|
||||
cookie.set_max_age(Duration::zero());
|
||||
cookie.set_expires(OffsetDateTime::now_utc() - Duration::days(365));
|
||||
self.delta_cookies.replace(DeltaCookie::removed(cookie));
|
||||
} else {
|
||||
self.delta_cookies.remove(cookie.name());
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes `cookie` from this jar completely. This method differs from
|
||||
/// `remove` in that no delta cookie is created under any condition. Neither
|
||||
/// the `delta` nor `iter` methods will return a cookie that is removed
|
||||
/// using this method.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Removing an _original_ cookie; no _removal_ cookie is generated:
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie};
|
||||
/// use time::Duration;
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
///
|
||||
/// // Add an original cookie and a new cookie.
|
||||
/// jar.add_original(Cookie::new("name", "value"));
|
||||
/// jar.add(Cookie::new("key", "value"));
|
||||
/// assert_eq!(jar.delta().count(), 1);
|
||||
/// assert_eq!(jar.iter().count(), 2);
|
||||
///
|
||||
/// // Now force remove the original cookie.
|
||||
/// jar.force_remove(Cookie::new("name", "value"));
|
||||
/// assert_eq!(jar.delta().count(), 1);
|
||||
/// assert_eq!(jar.iter().count(), 1);
|
||||
///
|
||||
/// // Now force remove the new cookie.
|
||||
/// jar.force_remove(Cookie::new("key", "value"));
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
/// assert_eq!(jar.iter().count(), 0);
|
||||
/// ```
|
||||
pub fn force_remove<'a>(&mut self, cookie: Cookie<'a>) {
|
||||
self.original_cookies.remove(cookie.name());
|
||||
self.delta_cookies.remove(cookie.name());
|
||||
}
|
||||
|
||||
/// Removes all cookies from this cookie jar.
|
||||
#[deprecated(
|
||||
since = "0.7.0",
|
||||
note = "calling this method may not remove \
|
||||
all cookies since the path and domain are not specified; use \
|
||||
`remove` instead"
|
||||
)]
|
||||
pub fn clear(&mut self) {
|
||||
self.delta_cookies.clear();
|
||||
for delta in mem::take(&mut self.original_cookies) {
|
||||
self.remove(delta.cookie);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over cookies that represent the changes to this jar
|
||||
/// over time. These cookies can be rendered directly as `Set-Cookie` header
|
||||
/// values to affect the changes made to this jar on the client.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie};
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.add_original(Cookie::new("name", "value"));
|
||||
/// jar.add_original(Cookie::new("second", "two"));
|
||||
///
|
||||
/// // Add new cookies.
|
||||
/// jar.add(Cookie::new("new", "third"));
|
||||
/// jar.add(Cookie::new("another", "fourth"));
|
||||
/// jar.add(Cookie::new("yac", "fifth"));
|
||||
///
|
||||
/// // Remove some cookies.
|
||||
/// jar.remove(Cookie::named("name"));
|
||||
/// jar.remove(Cookie::named("another"));
|
||||
///
|
||||
/// // Delta contains two new cookies ("new", "yac") and a removal ("name").
|
||||
/// assert_eq!(jar.delta().count(), 3);
|
||||
/// ```
|
||||
pub fn delta(&self) -> Delta<'_> {
|
||||
Delta {
|
||||
iter: self.delta_cookies.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over all of the cookies present in this jar.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie};
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
///
|
||||
/// jar.add_original(Cookie::new("name", "value"));
|
||||
/// jar.add_original(Cookie::new("second", "two"));
|
||||
///
|
||||
/// jar.add(Cookie::new("new", "third"));
|
||||
/// jar.add(Cookie::new("another", "fourth"));
|
||||
/// jar.add(Cookie::new("yac", "fifth"));
|
||||
///
|
||||
/// jar.remove(Cookie::named("name"));
|
||||
/// jar.remove(Cookie::named("another"));
|
||||
///
|
||||
/// // There are three cookies in the jar: "second", "new", and "yac".
|
||||
/// # assert_eq!(jar.iter().count(), 3);
|
||||
/// for cookie in jar.iter() {
|
||||
/// match cookie.name() {
|
||||
/// "second" => assert_eq!(cookie.value(), "two"),
|
||||
/// "new" => assert_eq!(cookie.value(), "third"),
|
||||
/// "yac" => assert_eq!(cookie.value(), "fifth"),
|
||||
/// _ => unreachable!("there are only three cookies in the jar")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn iter(&self) -> Iter<'_> {
|
||||
Iter {
|
||||
delta_cookies: self
|
||||
.delta_cookies
|
||||
.iter()
|
||||
.chain(self.original_cookies.difference(&self.delta_cookies)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `PrivateJar` with `self` as its parent jar using the key `key`
|
||||
/// to sign/encrypt and verify/decrypt cookies added/retrieved from the
|
||||
/// child jar.
|
||||
///
|
||||
/// Any modifications to the child jar will be reflected on the parent jar,
|
||||
/// and any retrievals from the child jar will be made from the parent jar.
|
||||
///
|
||||
/// This method is only available when the `secure` feature is enabled.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{Cookie, CookieJar, Key};
|
||||
///
|
||||
/// // Generate a secure key.
|
||||
/// let key = Key::generate();
|
||||
///
|
||||
/// // Add a private (signed + encrypted) cookie.
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.private(&key).add(Cookie::new("private", "text"));
|
||||
///
|
||||
/// // The cookie's contents are encrypted.
|
||||
/// assert_ne!(jar.get("private").unwrap().value(), "text");
|
||||
///
|
||||
/// // They can be decrypted and verified through the child jar.
|
||||
/// assert_eq!(jar.private(&key).get("private").unwrap().value(), "text");
|
||||
///
|
||||
/// // A tampered with cookie does not validate but still exists.
|
||||
/// let mut cookie = jar.get("private").unwrap().clone();
|
||||
/// jar.add(Cookie::new("private", cookie.value().to_string() + "!"));
|
||||
/// assert!(jar.private(&key).get("private").is_none());
|
||||
/// assert!(jar.get("private").is_some());
|
||||
/// ```
|
||||
#[cfg(feature = "secure-cookies")]
|
||||
pub fn private(&mut self, key: &Key) -> PrivateJar<'_> {
|
||||
PrivateJar::new(self, key)
|
||||
}
|
||||
|
||||
/// Returns a `SignedJar` with `self` as its parent jar using the key `key`
|
||||
/// to sign/verify cookies added/retrieved from the child jar.
|
||||
///
|
||||
/// Any modifications to the child jar will be reflected on the parent jar,
|
||||
/// and any retrievals from the child jar will be made from the parent jar.
|
||||
///
|
||||
/// This method is only available when the `secure` feature is enabled.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{Cookie, CookieJar, Key};
|
||||
///
|
||||
/// // Generate a secure key.
|
||||
/// let key = Key::generate();
|
||||
///
|
||||
/// // Add a signed cookie.
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.signed(&key).add(Cookie::new("signed", "text"));
|
||||
///
|
||||
/// // The cookie's contents are signed but still in plaintext.
|
||||
/// assert_ne!(jar.get("signed").unwrap().value(), "text");
|
||||
/// assert!(jar.get("signed").unwrap().value().contains("text"));
|
||||
///
|
||||
/// // They can be verified through the child jar.
|
||||
/// assert_eq!(jar.signed(&key).get("signed").unwrap().value(), "text");
|
||||
///
|
||||
/// // A tampered with cookie does not validate but still exists.
|
||||
/// let mut cookie = jar.get("signed").unwrap().clone();
|
||||
/// jar.add(Cookie::new("signed", cookie.value().to_string() + "!"));
|
||||
/// assert!(jar.signed(&key).get("signed").is_none());
|
||||
/// assert!(jar.get("signed").is_some());
|
||||
/// ```
|
||||
#[cfg(feature = "secure-cookies")]
|
||||
pub fn signed(&mut self, key: &Key) -> SignedJar<'_> {
|
||||
SignedJar::new(self, key)
|
||||
}
|
||||
}
|
||||
|
||||
use std::collections::hash_set::Iter as HashSetIter;
|
||||
|
||||
/// Iterator over the changes to a cookie jar.
|
||||
pub struct Delta<'a> {
|
||||
iter: HashSetIter<'a, DeltaCookie>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Delta<'a> {
|
||||
type Item = &'a Cookie<'static>;
|
||||
|
||||
fn next(&mut self) -> Option<&'a Cookie<'static>> {
|
||||
self.iter.next().map(|c| &c.cookie)
|
||||
}
|
||||
}
|
||||
|
||||
use std::collections::hash_map::RandomState;
|
||||
use std::collections::hash_set::Difference;
|
||||
use std::iter::Chain;
|
||||
|
||||
/// Iterator over all of the cookies in a jar.
|
||||
pub struct Iter<'a> {
|
||||
delta_cookies:
|
||||
Chain<HashSetIter<'a, DeltaCookie>, Difference<'a, DeltaCookie, RandomState>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = &'a Cookie<'static>;
|
||||
|
||||
fn next(&mut self) -> Option<&'a Cookie<'static>> {
|
||||
for cookie in self.delta_cookies.by_ref() {
|
||||
if !cookie.removed {
|
||||
return Some(&*cookie);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[cfg(feature = "secure-cookies")]
|
||||
use super::Key;
|
||||
use super::{Cookie, CookieJar};
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn simple() {
|
||||
let mut c = CookieJar::new();
|
||||
|
||||
c.add(Cookie::new("test", ""));
|
||||
c.add(Cookie::new("test2", ""));
|
||||
c.remove(Cookie::named("test"));
|
||||
|
||||
assert!(c.get("test").is_none());
|
||||
assert!(c.get("test2").is_some());
|
||||
|
||||
c.add(Cookie::new("test3", ""));
|
||||
c.clear();
|
||||
|
||||
assert!(c.get("test").is_none());
|
||||
assert!(c.get("test2").is_none());
|
||||
assert!(c.get("test3").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jar_is_send() {
|
||||
fn is_send<T: Send>(_: T) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
assert!(is_send(CookieJar::new()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "secure-cookies")]
|
||||
fn iter() {
|
||||
let key = Key::generate();
|
||||
let mut c = CookieJar::new();
|
||||
|
||||
c.add_original(Cookie::new("original", "original"));
|
||||
|
||||
c.add(Cookie::new("test", "test"));
|
||||
c.add(Cookie::new("test2", "test2"));
|
||||
c.add(Cookie::new("test3", "test3"));
|
||||
assert_eq!(c.iter().count(), 4);
|
||||
|
||||
c.signed(&key).add(Cookie::new("signed", "signed"));
|
||||
c.private(&key).add(Cookie::new("encrypted", "encrypted"));
|
||||
assert_eq!(c.iter().count(), 6);
|
||||
|
||||
c.remove(Cookie::named("test"));
|
||||
assert_eq!(c.iter().count(), 5);
|
||||
|
||||
c.remove(Cookie::named("signed"));
|
||||
c.remove(Cookie::named("test2"));
|
||||
assert_eq!(c.iter().count(), 3);
|
||||
|
||||
c.add(Cookie::new("test2", "test2"));
|
||||
assert_eq!(c.iter().count(), 4);
|
||||
|
||||
c.remove(Cookie::named("test2"));
|
||||
assert_eq!(c.iter().count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "secure-cookies")]
|
||||
fn delta() {
|
||||
use std::collections::HashMap;
|
||||
use time::Duration;
|
||||
|
||||
let mut c = CookieJar::new();
|
||||
|
||||
c.add_original(Cookie::new("original", "original"));
|
||||
c.add_original(Cookie::new("original1", "original1"));
|
||||
|
||||
c.add(Cookie::new("test", "test"));
|
||||
c.add(Cookie::new("test2", "test2"));
|
||||
c.add(Cookie::new("test3", "test3"));
|
||||
c.add(Cookie::new("test4", "test4"));
|
||||
|
||||
c.remove(Cookie::named("test"));
|
||||
c.remove(Cookie::named("original"));
|
||||
|
||||
assert_eq!(c.delta().count(), 4);
|
||||
|
||||
let names: HashMap<_, _> = c.delta().map(|c| (c.name(), c.max_age())).collect();
|
||||
|
||||
assert!(names.get("test2").unwrap().is_none());
|
||||
assert!(names.get("test3").unwrap().is_none());
|
||||
assert!(names.get("test4").unwrap().is_none());
|
||||
assert_eq!(names.get("original").unwrap(), &Some(Duration::zero()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_original() {
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add_original(Cookie::new("original_a", "a"));
|
||||
jar.add_original(Cookie::new("original_b", "b"));
|
||||
assert_eq!(jar.get("original_a").unwrap().value(), "a");
|
||||
|
||||
jar.add(Cookie::new("original_a", "av2"));
|
||||
assert_eq!(jar.get("original_a").unwrap().value(), "av2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_delta() {
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add(Cookie::new("name", "val"));
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
|
||||
jar.remove(Cookie::named("name"));
|
||||
assert_eq!(jar.delta().count(), 0);
|
||||
|
||||
jar.add_original(Cookie::new("name", "val"));
|
||||
assert_eq!(jar.delta().count(), 0);
|
||||
|
||||
jar.remove(Cookie::named("name"));
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
|
||||
jar.add(Cookie::new("name", "val"));
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
|
||||
jar.remove(Cookie::named("name"));
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_remove_add() {
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add_original(Cookie::new("name", "val"));
|
||||
assert_eq!(jar.delta().count(), 0);
|
||||
|
||||
jar.remove(Cookie::named("name"));
|
||||
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
|
||||
// The cookie's been deleted. Another original doesn't change that.
|
||||
jar.add_original(Cookie::new("name", "val"));
|
||||
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
|
||||
jar.remove(Cookie::named("name"));
|
||||
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
|
||||
jar.add(Cookie::new("name", "val"));
|
||||
assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1);
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
|
||||
jar.remove(Cookie::named("name"));
|
||||
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_remove() {
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add_original(Cookie::new("name", "val"));
|
||||
assert_eq!(jar.delta().count(), 0);
|
||||
|
||||
jar.add(Cookie::new("name", "val"));
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1);
|
||||
|
||||
jar.remove(Cookie::named("name"));
|
||||
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_with_path() {
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add_original(Cookie::build("name", "val").finish());
|
||||
assert_eq!(jar.iter().count(), 1);
|
||||
assert_eq!(jar.delta().count(), 0);
|
||||
assert_eq!(jar.iter().filter(|c| c.path().is_none()).count(), 1);
|
||||
|
||||
jar.remove(Cookie::build("name", "").path("/").finish());
|
||||
assert_eq!(jar.iter().count(), 0);
|
||||
assert_eq!(jar.delta().count(), 1);
|
||||
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
|
||||
assert_eq!(jar.delta().filter(|c| c.path() == Some("/")).count(), 1);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,467 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
use std::cmp;
|
||||
use std::convert::From;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::str::Utf8Error;
|
||||
|
||||
use percent_encoding::percent_decode;
|
||||
use time::Duration;
|
||||
|
||||
use super::{Cookie, CookieStr, SameSite};
|
||||
|
||||
use crate::time_parser;
|
||||
|
||||
/// Enum corresponding to a parsing error.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ParseError {
|
||||
/// The cookie did not contain a name/value pair.
|
||||
MissingPair,
|
||||
/// The cookie's name was empty.
|
||||
EmptyName,
|
||||
/// Decoding the cookie's name or value resulted in invalid UTF-8.
|
||||
Utf8Error(Utf8Error),
|
||||
/// It is discouraged to exhaustively match on this enum as its variants may
|
||||
/// grow without a breaking-change bump in version numbers.
|
||||
#[doc(hidden)]
|
||||
__Nonexhasutive,
|
||||
}
|
||||
|
||||
impl ParseError {
|
||||
/// Returns a description of this error as a string
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match *self {
|
||||
ParseError::MissingPair => "the cookie is missing a name/value pair",
|
||||
ParseError::EmptyName => "the cookie's name is empty",
|
||||
ParseError::Utf8Error(_) => {
|
||||
"decoding the cookie's name or value resulted in invalid UTF-8"
|
||||
}
|
||||
ParseError::__Nonexhasutive => unreachable!("__Nonexhasutive ParseError"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Utf8Error> for ParseError {
|
||||
fn from(error: Utf8Error) -> ParseError {
|
||||
ParseError::Utf8Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseError {}
|
||||
|
||||
fn indexes_of(needle: &str, haystack: &str) -> Option<(usize, usize)> {
|
||||
let haystack_start = haystack.as_ptr() as usize;
|
||||
let needle_start = needle.as_ptr() as usize;
|
||||
|
||||
if needle_start < haystack_start {
|
||||
return None;
|
||||
}
|
||||
|
||||
if (needle_start + needle.len()) > (haystack_start + haystack.len()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start = needle_start - haystack_start;
|
||||
let end = start + needle.len();
|
||||
Some((start, end))
|
||||
}
|
||||
|
||||
fn name_val_decoded(
|
||||
name: &str,
|
||||
val: &str,
|
||||
) -> Result<(CookieStr, CookieStr), ParseError> {
|
||||
let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?;
|
||||
let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?;
|
||||
let name = CookieStr::Concrete(Cow::Owned(decoded_name.into_owned()));
|
||||
let val = CookieStr::Concrete(Cow::Owned(decoded_value.into_owned()));
|
||||
|
||||
Ok((name, val))
|
||||
}
|
||||
|
||||
// This function does the real parsing but _does not_ set the `cookie_string` in
|
||||
// the returned cookie object. This only exists so that the borrow to `s` is
|
||||
// returned at the end of the call, allowing the `cookie_string` field to be
|
||||
// set in the outer `parse` function.
|
||||
fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
|
||||
let mut attributes = s.split(';');
|
||||
let key_value = match attributes.next() {
|
||||
Some(s) => s,
|
||||
_ => panic!(),
|
||||
};
|
||||
|
||||
// Determine the name = val.
|
||||
let (name, value) = match key_value.find('=') {
|
||||
Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()),
|
||||
None => return Err(ParseError::MissingPair),
|
||||
};
|
||||
|
||||
if name.is_empty() {
|
||||
return Err(ParseError::EmptyName);
|
||||
}
|
||||
|
||||
// Create a cookie with all of the defaults. We'll fill things in while we
|
||||
// iterate through the parameters below.
|
||||
let (name, value) = if decode {
|
||||
name_val_decoded(name, value)?
|
||||
} else {
|
||||
let name_indexes = indexes_of(name, s).expect("name sub");
|
||||
let value_indexes = indexes_of(value, s).expect("value sub");
|
||||
let name = CookieStr::Indexed(name_indexes.0, name_indexes.1);
|
||||
let value = CookieStr::Indexed(value_indexes.0, value_indexes.1);
|
||||
|
||||
(name, value)
|
||||
};
|
||||
|
||||
let mut cookie = Cookie {
|
||||
name,
|
||||
value,
|
||||
cookie_string: None,
|
||||
expires: None,
|
||||
max_age: None,
|
||||
domain: None,
|
||||
path: None,
|
||||
secure: None,
|
||||
http_only: None,
|
||||
same_site: None,
|
||||
};
|
||||
|
||||
for attr in attributes {
|
||||
let (key, value) = match attr.find('=') {
|
||||
Some(i) => (attr[..i].trim(), Some(attr[(i + 1)..].trim())),
|
||||
None => (attr.trim(), None),
|
||||
};
|
||||
|
||||
match (&*key.to_ascii_lowercase(), value) {
|
||||
("secure", _) => cookie.secure = Some(true),
|
||||
("httponly", _) => cookie.http_only = Some(true),
|
||||
("max-age", Some(v)) => {
|
||||
// See RFC 6265 Section 5.2.2, negative values indicate that the
|
||||
// earliest possible expiration time should be used, so set the
|
||||
// max age as 0 seconds.
|
||||
cookie.max_age = match v.parse() {
|
||||
Ok(val) if val <= 0 => Some(Duration::zero()),
|
||||
Ok(val) => {
|
||||
// Don't panic if the max age seconds is greater than what's supported by
|
||||
// `Duration`.
|
||||
let val = cmp::min(val, Duration::max_value().whole_seconds());
|
||||
Some(Duration::seconds(val))
|
||||
}
|
||||
Err(_) => continue,
|
||||
};
|
||||
}
|
||||
("domain", Some(mut domain)) if !domain.is_empty() => {
|
||||
if domain.starts_with('.') {
|
||||
domain = &domain[1..];
|
||||
}
|
||||
|
||||
let (i, j) = indexes_of(domain, s).expect("domain sub");
|
||||
cookie.domain = Some(CookieStr::Indexed(i, j));
|
||||
}
|
||||
("path", Some(v)) => {
|
||||
let (i, j) = indexes_of(v, s).expect("path sub");
|
||||
cookie.path = Some(CookieStr::Indexed(i, j));
|
||||
}
|
||||
("samesite", Some(v)) => {
|
||||
if v.eq_ignore_ascii_case("strict") {
|
||||
cookie.same_site = Some(SameSite::Strict);
|
||||
} else if v.eq_ignore_ascii_case("lax") {
|
||||
cookie.same_site = Some(SameSite::Lax);
|
||||
} else if v.eq_ignore_ascii_case("none") {
|
||||
cookie.same_site = Some(SameSite::None);
|
||||
} else {
|
||||
// We do nothing here, for now. When/if the `SameSite`
|
||||
// attribute becomes standard, the spec says that we should
|
||||
// ignore this cookie, i.e, fail to parse it, when an
|
||||
// invalid value is passed in. The draft is at
|
||||
// http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html.
|
||||
}
|
||||
}
|
||||
("expires", Some(v)) => {
|
||||
// Try parsing with three date formats according to
|
||||
// http://tools.ietf.org/html/rfc2616#section-3.3.1. Try
|
||||
// additional ones as encountered in the real world.
|
||||
let tm = time_parser::parse_http_date(v)
|
||||
.or_else(|| time::parse(v, "%a, %d-%b-%Y %H:%M:%S").ok());
|
||||
|
||||
if let Some(time) = tm {
|
||||
cookie.expires = Some(time.assume_utc())
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// We're going to be permissive here. If we have no idea what
|
||||
// this is, then it's something nonstandard. We're not going to
|
||||
// store it (because it's not compliant), but we're also not
|
||||
// going to emit an error.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cookie)
|
||||
}
|
||||
|
||||
pub fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result<Cookie<'c>, ParseError>
|
||||
where
|
||||
S: Into<Cow<'c, str>>,
|
||||
{
|
||||
let s = cow.into();
|
||||
let mut cookie = parse_inner(&s, decode)?;
|
||||
cookie.cookie_string = Some(s);
|
||||
Ok(cookie)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Cookie, SameSite};
|
||||
use time::{Duration, PrimitiveDateTime};
|
||||
|
||||
macro_rules! assert_eq_parse {
|
||||
($string:expr, $expected:expr) => {
|
||||
let cookie = match Cookie::parse($string) {
|
||||
Ok(cookie) => cookie,
|
||||
Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e),
|
||||
};
|
||||
|
||||
assert_eq!(cookie, $expected);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_ne_parse {
|
||||
($string:expr, $expected:expr) => {
|
||||
let cookie = match Cookie::parse($string) {
|
||||
Ok(cookie) => cookie,
|
||||
Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e),
|
||||
};
|
||||
|
||||
assert_ne!(cookie, $expected);
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_same_site() {
|
||||
let expected = Cookie::build("foo", "bar")
|
||||
.same_site(SameSite::Lax)
|
||||
.finish();
|
||||
|
||||
assert_eq_parse!("foo=bar; SameSite=Lax", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=lax", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=LAX", expected);
|
||||
assert_eq_parse!("foo=bar; samesite=Lax", expected);
|
||||
assert_eq_parse!("foo=bar; SAMESITE=Lax", expected);
|
||||
|
||||
let expected = Cookie::build("foo", "bar")
|
||||
.same_site(SameSite::Strict)
|
||||
.finish();
|
||||
|
||||
assert_eq_parse!("foo=bar; SameSite=Strict", expected);
|
||||
assert_eq_parse!("foo=bar; SameSITE=Strict", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=strict", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=STrICT", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=STRICT", expected);
|
||||
|
||||
let expected = Cookie::build("foo", "bar")
|
||||
.same_site(SameSite::None)
|
||||
.finish();
|
||||
|
||||
assert_eq_parse!("foo=bar; SameSite=None", expected);
|
||||
assert_eq_parse!("foo=bar; SameSITE=None", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=nOne", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=NoNE", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=NONE", expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert!(Cookie::parse("bar").is_err());
|
||||
assert!(Cookie::parse("=bar").is_err());
|
||||
assert!(Cookie::parse(" =bar").is_err());
|
||||
assert!(Cookie::parse("foo=").is_ok());
|
||||
|
||||
let expected = Cookie::build("foo", "bar=baz").finish();
|
||||
assert_eq_parse!("foo=bar=baz", expected);
|
||||
|
||||
let mut expected = Cookie::build("foo", "bar").finish();
|
||||
assert_eq_parse!("foo=bar", expected);
|
||||
assert_eq_parse!("foo = bar", expected);
|
||||
assert_eq_parse!(" foo=bar ", expected);
|
||||
assert_eq_parse!(" foo=bar ;Domain=", expected);
|
||||
assert_eq_parse!(" foo=bar ;Domain= ", expected);
|
||||
assert_eq_parse!(" foo=bar ;Ignored", expected);
|
||||
|
||||
let mut unexpected = Cookie::build("foo", "bar").http_only(false).finish();
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly", unexpected);
|
||||
assert_ne_parse!(" foo=bar; httponly", unexpected);
|
||||
|
||||
expected.set_http_only(true);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly", expected);
|
||||
assert_eq_parse!(" foo=bar ;httponly", expected);
|
||||
assert_eq_parse!(" foo=bar ;HTTPONLY=whatever", expected);
|
||||
assert_eq_parse!(" foo=bar ; sekure; HTTPONLY", expected);
|
||||
|
||||
expected.set_secure(true);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure", expected);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure=aaaa", expected);
|
||||
|
||||
unexpected.set_http_only(true);
|
||||
unexpected.set_secure(true);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; skeure", unexpected);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; =secure", unexpected);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly;", unexpected);
|
||||
|
||||
unexpected.set_secure(false);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
|
||||
|
||||
expected.set_max_age(Duration::zero());
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected);
|
||||
|
||||
expected.set_max_age(Duration::minutes(1));
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 60 ", expected);
|
||||
|
||||
expected.set_max_age(Duration::seconds(4));
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4", expected);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 4 ", expected);
|
||||
|
||||
unexpected.set_secure(true);
|
||||
unexpected.set_max_age(Duration::minutes(1));
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=122", unexpected);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 38 ", unexpected);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=51", unexpected);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", unexpected);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0", unexpected);
|
||||
|
||||
expected.set_path("/");
|
||||
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/", expected);
|
||||
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/", expected);
|
||||
|
||||
expected.set_path("/foo");
|
||||
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", expected);
|
||||
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/foo", expected);
|
||||
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path=/foo", expected);
|
||||
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path = /foo", expected);
|
||||
|
||||
unexpected.set_max_age(Duration::seconds(4));
|
||||
unexpected.set_path("/bar");
|
||||
assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", unexpected);
|
||||
assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/baz", unexpected);
|
||||
|
||||
expected.set_domain("www.foo.com");
|
||||
assert_eq_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=www.foo.com",
|
||||
expected
|
||||
);
|
||||
|
||||
expected.set_domain("foo.com");
|
||||
assert_eq_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=foo.com",
|
||||
expected
|
||||
);
|
||||
assert_eq_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=FOO.COM",
|
||||
expected
|
||||
);
|
||||
|
||||
unexpected.set_path("/foo");
|
||||
unexpected.set_domain("bar.com");
|
||||
assert_ne_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=foo.com",
|
||||
unexpected
|
||||
);
|
||||
assert_ne_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=FOO.COM",
|
||||
unexpected
|
||||
);
|
||||
|
||||
let time_str = "Wed, 21 Oct 2015 07:28:00 GMT";
|
||||
let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S")
|
||||
.unwrap()
|
||||
.assume_utc();
|
||||
expected.set_expires(expires);
|
||||
assert_eq_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
|
||||
expected
|
||||
);
|
||||
|
||||
unexpected.set_domain("foo.com");
|
||||
let bad_expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%S:%M")
|
||||
.unwrap()
|
||||
.assume_utc();
|
||||
expected.set_expires(bad_expires);
|
||||
assert_ne_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT",
|
||||
unexpected
|
||||
);
|
||||
|
||||
expected.set_expires(expires);
|
||||
expected.set_same_site(SameSite::Lax);
|
||||
assert_eq_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT; \
|
||||
SameSite=Lax",
|
||||
expected
|
||||
);
|
||||
expected.set_same_site(SameSite::Strict);
|
||||
assert_eq_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT; \
|
||||
SameSite=Strict",
|
||||
expected
|
||||
);
|
||||
expected.set_same_site(SameSite::None);
|
||||
assert_eq_parse!(
|
||||
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT; \
|
||||
SameSite=None",
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_characters() {
|
||||
let expected = Cookie::new("foo", "b%2Fr");
|
||||
assert_eq_parse!("foo=b%2Fr", expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_characters_encoded() {
|
||||
let expected = Cookie::new("foo", "b/r");
|
||||
let cookie = match Cookie::parse_encoded("foo=b%2Fr") {
|
||||
Ok(cookie) => cookie,
|
||||
Err(e) => panic!("Failed to parse: {:?}", e),
|
||||
};
|
||||
|
||||
assert_eq!(cookie, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn do_not_panic_on_large_max_ages() {
|
||||
let max_duration = Duration::max_value();
|
||||
let expected = Cookie::build("foo", "bar")
|
||||
.max_age_time(max_duration)
|
||||
.finish();
|
||||
let overflow_duration = max_duration
|
||||
.checked_add(Duration::nanoseconds(1))
|
||||
.unwrap_or(max_duration);
|
||||
assert_eq_parse!(
|
||||
format!(" foo=bar; Max-Age={:?}", overflow_duration.whole_seconds()),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
use ring::hkdf::{Algorithm, KeyType, Prk, HKDF_SHA256};
|
||||
use ring::rand::{SecureRandom, SystemRandom};
|
||||
|
||||
use super::private::KEY_LEN as PRIVATE_KEY_LEN;
|
||||
use super::signed::KEY_LEN as SIGNED_KEY_LEN;
|
||||
|
||||
static HKDF_DIGEST: Algorithm = HKDF_SHA256;
|
||||
const KEYS_INFO: &[&[u8]] = &[b"COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM"];
|
||||
|
||||
/// A cryptographic master key for use with `Signed` and/or `Private` jars.
|
||||
///
|
||||
/// This structure encapsulates secure, cryptographic keys for use with both
|
||||
/// [PrivateJar](struct.PrivateJar.html) and [SignedJar](struct.SignedJar.html).
|
||||
/// It can be derived from a single master key via
|
||||
/// [from_master](#method.from_master) or generated from a secure random source
|
||||
/// via [generate](#method.generate). A single instance of `Key` can be used for
|
||||
/// both a `PrivateJar` and a `SignedJar`.
|
||||
///
|
||||
/// This type is only available when the `secure` feature is enabled.
|
||||
#[derive(Clone)]
|
||||
pub struct Key {
|
||||
signing_key: [u8; SIGNED_KEY_LEN],
|
||||
encryption_key: [u8; PRIVATE_KEY_LEN],
|
||||
}
|
||||
|
||||
impl KeyType for &Key {
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
SIGNED_KEY_LEN + PRIVATE_KEY_LEN
|
||||
}
|
||||
}
|
||||
|
||||
impl Key {
|
||||
/// Derives new signing/encryption keys from a master key.
|
||||
///
|
||||
/// The master key must be at least 256-bits (32 bytes). For security, the
|
||||
/// master key _must_ be cryptographically random. The keys are derived
|
||||
/// deterministically from the master key.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `key` is less than 32 bytes in length.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Key;
|
||||
///
|
||||
/// # /*
|
||||
/// let master_key = { /* a cryptographically random key >= 32 bytes */ };
|
||||
/// # */
|
||||
/// # let master_key: &Vec<u8> = &(0..32).collect();
|
||||
///
|
||||
/// let key = Key::from_master(master_key);
|
||||
/// ```
|
||||
pub fn from_master(key: &[u8]) -> Key {
|
||||
if key.len() < 32 {
|
||||
panic!(
|
||||
"bad master key length: expected at least 32 bytes, found {}",
|
||||
key.len()
|
||||
);
|
||||
}
|
||||
|
||||
// An empty `Key` structure; will be filled in with HKDF derived keys.
|
||||
let mut output_key = Key {
|
||||
signing_key: [0; SIGNED_KEY_LEN],
|
||||
encryption_key: [0; PRIVATE_KEY_LEN],
|
||||
};
|
||||
|
||||
// Expand the master key into two HKDF generated keys.
|
||||
let mut both_keys = [0; SIGNED_KEY_LEN + PRIVATE_KEY_LEN];
|
||||
let prk = Prk::new_less_safe(HKDF_DIGEST, key);
|
||||
let okm = prk.expand(KEYS_INFO, &output_key).expect("okm expand");
|
||||
okm.fill(&mut both_keys).expect("fill keys");
|
||||
|
||||
// Copy the key parts into their respective fields.
|
||||
output_key
|
||||
.signing_key
|
||||
.copy_from_slice(&both_keys[..SIGNED_KEY_LEN]);
|
||||
output_key
|
||||
.encryption_key
|
||||
.copy_from_slice(&both_keys[SIGNED_KEY_LEN..]);
|
||||
output_key
|
||||
}
|
||||
|
||||
/// Generates signing/encryption keys from a secure, random source. Keys are
|
||||
/// generated non-deterministically.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if randomness cannot be retrieved from the operating system. See
|
||||
/// [try_generate](#method.try_generate) for a non-panicking version.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Key;
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// ```
|
||||
pub fn generate() -> Key {
|
||||
Self::try_generate().expect("failed to generate `Key` from randomness")
|
||||
}
|
||||
|
||||
/// Attempts to generate signing/encryption keys from a secure, random
|
||||
/// source. Keys are generated non-deterministically. If randomness cannot be
|
||||
/// retrieved from the underlying operating system, returns `None`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Key;
|
||||
///
|
||||
/// let key = Key::try_generate();
|
||||
/// ```
|
||||
pub fn try_generate() -> Option<Key> {
|
||||
let mut sign_key = [0; SIGNED_KEY_LEN];
|
||||
let mut enc_key = [0; PRIVATE_KEY_LEN];
|
||||
|
||||
let rng = SystemRandom::new();
|
||||
if rng.fill(&mut sign_key).is_err() || rng.fill(&mut enc_key).is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Key {
|
||||
signing_key: sign_key,
|
||||
encryption_key: enc_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the raw bytes of a key suitable for signing cookies.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Key;
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let signing_key = key.signing();
|
||||
/// ```
|
||||
pub fn signing(&self) -> &[u8] {
|
||||
&self.signing_key[..]
|
||||
}
|
||||
|
||||
/// Returns the raw bytes of a key suitable for encrypting cookies.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::Key;
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let encryption_key = key.encryption();
|
||||
/// ```
|
||||
pub fn encryption(&self) -> &[u8] {
|
||||
&self.encryption_key[..]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Key;
|
||||
|
||||
#[test]
|
||||
fn deterministic_from_master() {
|
||||
let master_key: Vec<u8> = (0..32).collect();
|
||||
|
||||
let key_a = Key::from_master(&master_key);
|
||||
let key_b = Key::from_master(&master_key);
|
||||
|
||||
assert_eq!(key_a.signing(), key_b.signing());
|
||||
assert_eq!(key_a.encryption(), key_b.encryption());
|
||||
assert_ne!(key_a.encryption(), key_a.signing());
|
||||
|
||||
let master_key_2: Vec<u8> = (32..64).collect();
|
||||
let key_2 = Key::from_master(&master_key_2);
|
||||
|
||||
assert_ne!(key_2.signing(), key_a.signing());
|
||||
assert_ne!(key_2.encryption(), key_a.encryption());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_deterministic_generate() {
|
||||
let key_a = Key::generate();
|
||||
let key_b = Key::generate();
|
||||
|
||||
assert_ne!(key_a.signing(), key_b.signing());
|
||||
assert_ne!(key_a.encryption(), key_b.encryption());
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
#[cfg(test)]
|
||||
macro_rules! assert_simple_behaviour {
|
||||
($clear:expr, $secure:expr) => {{
|
||||
assert_eq!($clear.iter().count(), 0);
|
||||
|
||||
$secure.add(Cookie::new("name", "val"));
|
||||
assert_eq!($clear.iter().count(), 1);
|
||||
assert_eq!($secure.get("name").unwrap().value(), "val");
|
||||
assert_ne!($clear.get("name").unwrap().value(), "val");
|
||||
|
||||
$secure.add(Cookie::new("another", "two"));
|
||||
assert_eq!($clear.iter().count(), 2);
|
||||
|
||||
$clear.remove(Cookie::named("another"));
|
||||
assert_eq!($clear.iter().count(), 1);
|
||||
|
||||
$secure.remove(Cookie::named("name"));
|
||||
assert_eq!($clear.iter().count(), 0);
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! assert_secure_behaviour {
|
||||
($clear:expr, $secure:expr) => {{
|
||||
$secure.add(Cookie::new("secure", "secure"));
|
||||
assert!($clear.get("secure").unwrap().value() != "secure");
|
||||
assert!($secure.get("secure").unwrap().value() == "secure");
|
||||
|
||||
let mut cookie = $clear.get("secure").unwrap().clone();
|
||||
let new_val = format!("{}l", cookie.value());
|
||||
cookie.set_value(new_val);
|
||||
$clear.add(cookie);
|
||||
assert!($secure.get("secure").is_none());
|
||||
|
||||
let mut cookie = $clear.get("secure").unwrap().clone();
|
||||
cookie.set_value("foobar");
|
||||
$clear.add(cookie);
|
||||
assert!($secure.get("secure").is_none());
|
||||
}};
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
//! Fork of https://github.com/alexcrichton/cookie-rs
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod key;
|
||||
mod private;
|
||||
mod signed;
|
||||
|
||||
pub use self::key::*;
|
||||
pub use self::private::*;
|
||||
pub use self::signed::*;
|
@ -1,275 +0,0 @@
|
||||
use std::str;
|
||||
|
||||
use log::warn;
|
||||
use ring::aead::{Aad, Algorithm, Nonce, AES_256_GCM};
|
||||
use ring::aead::{LessSafeKey, UnboundKey};
|
||||
use ring::rand::{SecureRandom, SystemRandom};
|
||||
|
||||
use super::Key;
|
||||
use crate::cookie::{Cookie, CookieJar};
|
||||
|
||||
// Keep these in sync, and keep the key len synced with the `private` docs as
|
||||
// well as the `KEYS_INFO` const in secure::Key.
|
||||
static ALGO: &Algorithm = &AES_256_GCM;
|
||||
const NONCE_LEN: usize = 12;
|
||||
pub const KEY_LEN: usize = 32;
|
||||
|
||||
/// A child cookie jar that provides authenticated encryption for its cookies.
|
||||
///
|
||||
/// A _private_ child jar signs and encrypts all the cookies added to it and
|
||||
/// verifies and decrypts cookies retrieved from it. Any cookies stored in a
|
||||
/// `PrivateJar` are simultaneously assured confidentiality, integrity, and
|
||||
/// authenticity. In other words, clients cannot discover nor tamper with the
|
||||
/// contents of a cookie, nor can they fabricate cookie data.
|
||||
///
|
||||
/// This type is only available when the `secure` feature is enabled.
|
||||
pub struct PrivateJar<'a> {
|
||||
parent: &'a mut CookieJar,
|
||||
key: [u8; KEY_LEN],
|
||||
}
|
||||
|
||||
impl<'a> PrivateJar<'a> {
|
||||
/// Creates a new child `PrivateJar` with parent `parent` and key `key`.
|
||||
/// This method is typically called indirectly via the `signed` method of
|
||||
/// `CookieJar`.
|
||||
#[doc(hidden)]
|
||||
pub fn new(parent: &'a mut CookieJar, key: &Key) -> PrivateJar<'a> {
|
||||
let mut key_array = [0u8; KEY_LEN];
|
||||
key_array.copy_from_slice(key.encryption());
|
||||
PrivateJar {
|
||||
parent,
|
||||
key: key_array,
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a sealed value `str` and a key name `name`, where the nonce is
|
||||
/// prepended to the original value and then both are Base64 encoded,
|
||||
/// verifies and decrypts the sealed value and returns it. If there's a
|
||||
/// problem, returns an `Err` with a string describing the issue.
|
||||
fn unseal(&self, name: &str, value: &str) -> Result<String, &'static str> {
|
||||
let mut data = base64::decode(value).map_err(|_| "bad base64 value")?;
|
||||
if data.len() <= NONCE_LEN {
|
||||
return Err("length of decoded data is <= NONCE_LEN");
|
||||
}
|
||||
|
||||
let ad = Aad::from(name.as_bytes());
|
||||
let key = LessSafeKey::new(
|
||||
UnboundKey::new(&ALGO, &self.key).expect("matching key length"),
|
||||
);
|
||||
let (nonce, mut sealed) = data.split_at_mut(NONCE_LEN);
|
||||
let nonce =
|
||||
Nonce::try_assume_unique_for_key(nonce).expect("invalid length of `nonce`");
|
||||
let unsealed = key
|
||||
.open_in_place(nonce, ad, &mut sealed)
|
||||
.map_err(|_| "invalid key/nonce/value: bad seal")?;
|
||||
|
||||
if let Ok(unsealed_utf8) = str::from_utf8(unsealed) {
|
||||
Ok(unsealed_utf8.to_string())
|
||||
} else {
|
||||
warn!(
|
||||
"Private cookie does not have utf8 content!
|
||||
It is likely the secret key used to encrypt them has been leaked.
|
||||
Please change it as soon as possible."
|
||||
);
|
||||
Err("bad unsealed utf8")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the `Cookie` inside this jar with the name `name`
|
||||
/// and authenticates and decrypts the cookie's value, returning a `Cookie`
|
||||
/// with the decrypted value. If the cookie cannot be found, or the cookie
|
||||
/// fails to authenticate or decrypt, `None` is returned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// let mut private_jar = jar.private(&key);
|
||||
/// assert!(private_jar.get("name").is_none());
|
||||
///
|
||||
/// private_jar.add(Cookie::new("name", "value"));
|
||||
/// assert_eq!(private_jar.get("name").unwrap().value(), "value");
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
|
||||
if let Some(cookie_ref) = self.parent.get(name) {
|
||||
let mut cookie = cookie_ref.clone();
|
||||
if let Ok(value) = self.unseal(name, cookie.value()) {
|
||||
cookie.set_value(value);
|
||||
return Some(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Adds `cookie` to the parent jar. The cookie's value is encrypted with
|
||||
/// authenticated encryption assuring confidentiality, integrity, and
|
||||
/// authenticity.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.private(&key).add(Cookie::new("name", "value"));
|
||||
///
|
||||
/// assert_ne!(jar.get("name").unwrap().value(), "value");
|
||||
/// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value");
|
||||
/// ```
|
||||
pub fn add(&mut self, mut cookie: Cookie<'static>) {
|
||||
self.encrypt_cookie(&mut cookie);
|
||||
|
||||
// Add the sealed cookie to the parent.
|
||||
self.parent.add(cookie);
|
||||
}
|
||||
|
||||
/// Adds an "original" `cookie` to parent jar. The cookie's value is
|
||||
/// encrypted with authenticated encryption assuring confidentiality,
|
||||
/// integrity, and authenticity. Adding an original cookie does not affect
|
||||
/// the [`CookieJar::delta()`](struct.CookieJar.html#method.delta)
|
||||
/// computation. This method is intended to be used to seed the cookie jar
|
||||
/// with cookies received from a client's HTTP message.
|
||||
///
|
||||
/// For accurate `delta` computations, this method should not be called
|
||||
/// after calling `remove`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.private(&key).add_original(Cookie::new("name", "value"));
|
||||
///
|
||||
/// assert_eq!(jar.iter().count(), 1);
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
/// ```
|
||||
pub fn add_original(&mut self, mut cookie: Cookie<'static>) {
|
||||
self.encrypt_cookie(&mut cookie);
|
||||
|
||||
// Add the sealed cookie to the parent.
|
||||
self.parent.add_original(cookie);
|
||||
}
|
||||
|
||||
/// Encrypts the cookie's value with
|
||||
/// authenticated encryption assuring confidentiality, integrity, and authenticity.
|
||||
fn encrypt_cookie(&self, cookie: &mut Cookie<'_>) {
|
||||
let name = cookie.name().as_bytes();
|
||||
let value = cookie.value().as_bytes();
|
||||
let data = encrypt_name_value(name, value, &self.key);
|
||||
|
||||
// Base64 encode the nonce and encrypted value.
|
||||
let sealed_value = base64::encode(&data);
|
||||
cookie.set_value(sealed_value);
|
||||
}
|
||||
|
||||
/// Removes `cookie` from the parent jar.
|
||||
///
|
||||
/// For correct removal, the passed in `cookie` must contain the same `path`
|
||||
/// and `domain` as the cookie that was initially set.
|
||||
///
|
||||
/// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more
|
||||
/// details.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// let mut private_jar = jar.private(&key);
|
||||
///
|
||||
/// private_jar.add(Cookie::new("name", "value"));
|
||||
/// assert!(private_jar.get("name").is_some());
|
||||
///
|
||||
/// private_jar.remove(Cookie::named("name"));
|
||||
/// assert!(private_jar.get("name").is_none());
|
||||
/// ```
|
||||
pub fn remove(&mut self, cookie: Cookie<'static>) {
|
||||
self.parent.remove(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt_name_value(name: &[u8], value: &[u8], key: &[u8]) -> Vec<u8> {
|
||||
// Create the `SealingKey` structure.
|
||||
let unbound = UnboundKey::new(&ALGO, key).expect("matching key length");
|
||||
let key = LessSafeKey::new(unbound);
|
||||
|
||||
// Create a vec to hold the [nonce | cookie value | overhead].
|
||||
let mut data = vec![0; NONCE_LEN + value.len() + ALGO.tag_len()];
|
||||
|
||||
// Randomly generate the nonce, then copy the cookie value as input.
|
||||
let (nonce, in_out) = data.split_at_mut(NONCE_LEN);
|
||||
let (in_out, tag) = in_out.split_at_mut(value.len());
|
||||
in_out.copy_from_slice(value);
|
||||
|
||||
// Randomly generate the nonce into the nonce piece.
|
||||
SystemRandom::new()
|
||||
.fill(nonce)
|
||||
.expect("couldn't random fill nonce");
|
||||
let nonce = Nonce::try_assume_unique_for_key(nonce).expect("invalid `nonce` length");
|
||||
|
||||
// Use cookie's name as associated data to prevent value swapping.
|
||||
let ad = Aad::from(name);
|
||||
let ad_tag = key
|
||||
.seal_in_place_separate_tag(nonce, ad, in_out)
|
||||
.expect("in-place seal");
|
||||
|
||||
// Copy the tag into the tag piece.
|
||||
tag.copy_from_slice(ad_tag.as_ref());
|
||||
|
||||
// Remove the overhead and return the sealed content.
|
||||
data
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{encrypt_name_value, Cookie, CookieJar, Key};
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let key = Key::generate();
|
||||
let mut jar = CookieJar::new();
|
||||
assert_simple_behaviour!(jar, jar.private(&key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private() {
|
||||
let key = Key::generate();
|
||||
let mut jar = CookieJar::new();
|
||||
assert_secure_behaviour!(jar, jar.private(&key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_utf8() {
|
||||
let key = Key::generate();
|
||||
let mut jar = CookieJar::new();
|
||||
|
||||
let name = "malicious";
|
||||
let mut assert_non_utf8 = |value: &[u8]| {
|
||||
let sealed = encrypt_name_value(name.as_bytes(), value, &key.encryption());
|
||||
let encoded = base64::encode(&sealed);
|
||||
assert_eq!(
|
||||
jar.private(&key).unseal(name, &encoded),
|
||||
Err("bad unsealed utf8")
|
||||
);
|
||||
jar.add(Cookie::new(name, encoded));
|
||||
assert_eq!(jar.private(&key).get(name), None);
|
||||
};
|
||||
|
||||
assert_non_utf8(&[0x72, 0xfb, 0xdf, 0x74]); // rûst in ISO/IEC 8859-1
|
||||
|
||||
let mut malicious =
|
||||
String::from(r#"{"id":"abc123??%X","admin":true}"#).into_bytes();
|
||||
malicious[8] |= 0b1100_0000;
|
||||
malicious[9] |= 0b1100_0000;
|
||||
assert_non_utf8(&malicious);
|
||||
}
|
||||
}
|
@ -1,184 +0,0 @@
|
||||
use ring::hmac::{self, sign, verify};
|
||||
|
||||
use super::Key;
|
||||
use crate::cookie::{Cookie, CookieJar};
|
||||
|
||||
// Keep these in sync, and keep the key len synced with the `signed` docs as
|
||||
// well as the `KEYS_INFO` const in secure::Key.
|
||||
static HMAC_DIGEST: hmac::Algorithm = hmac::HMAC_SHA256;
|
||||
const BASE64_DIGEST_LEN: usize = 44;
|
||||
pub const KEY_LEN: usize = 32;
|
||||
|
||||
/// A child cookie jar that authenticates its cookies.
|
||||
///
|
||||
/// A _signed_ child jar signs all the cookies added to it and verifies cookies
|
||||
/// retrieved from it. Any cookies stored in a `SignedJar` are assured integrity
|
||||
/// and authenticity. In other words, clients cannot tamper with the contents of
|
||||
/// a cookie nor can they fabricate cookie values, but the data is visible in
|
||||
/// plaintext.
|
||||
///
|
||||
/// This type is only available when the `secure` feature is enabled.
|
||||
pub struct SignedJar<'a> {
|
||||
parent: &'a mut CookieJar,
|
||||
key: hmac::Key,
|
||||
}
|
||||
|
||||
impl<'a> SignedJar<'a> {
|
||||
/// Creates a new child `SignedJar` with parent `parent` and key `key`. This
|
||||
/// method is typically called indirectly via the `signed` method of
|
||||
/// `CookieJar`.
|
||||
#[doc(hidden)]
|
||||
pub fn new(parent: &'a mut CookieJar, key: &Key) -> SignedJar<'a> {
|
||||
SignedJar {
|
||||
parent,
|
||||
key: hmac::Key::new(HMAC_DIGEST, key.signing()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a signed value `str` where the signature is prepended to `value`,
|
||||
/// verifies the signed value and returns it. If there's a problem, returns
|
||||
/// an `Err` with a string describing the issue.
|
||||
fn verify(&self, cookie_value: &str) -> Result<String, &'static str> {
|
||||
if cookie_value.len() < BASE64_DIGEST_LEN {
|
||||
return Err("length of value is <= BASE64_DIGEST_LEN");
|
||||
}
|
||||
|
||||
let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
|
||||
let sig = base64::decode(digest_str).map_err(|_| "bad base64 digest")?;
|
||||
|
||||
verify(&self.key, value.as_bytes(), &sig)
|
||||
.map(|_| value.to_string())
|
||||
.map_err(|_| "value did not verify")
|
||||
}
|
||||
|
||||
/// Returns a reference to the `Cookie` inside this jar with the name `name`
|
||||
/// and verifies the authenticity and integrity of the cookie's value,
|
||||
/// returning a `Cookie` with the authenticated value. If the cookie cannot
|
||||
/// be found, or the cookie fails to verify, `None` is returned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// let mut signed_jar = jar.signed(&key);
|
||||
/// assert!(signed_jar.get("name").is_none());
|
||||
///
|
||||
/// signed_jar.add(Cookie::new("name", "value"));
|
||||
/// assert_eq!(signed_jar.get("name").unwrap().value(), "value");
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
|
||||
if let Some(cookie_ref) = self.parent.get(name) {
|
||||
let mut cookie = cookie_ref.clone();
|
||||
if let Ok(value) = self.verify(cookie.value()) {
|
||||
cookie.set_value(value);
|
||||
return Some(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Adds `cookie` to the parent jar. The cookie's value is signed assuring
|
||||
/// integrity and authenticity.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.signed(&key).add(Cookie::new("name", "value"));
|
||||
///
|
||||
/// assert_ne!(jar.get("name").unwrap().value(), "value");
|
||||
/// assert!(jar.get("name").unwrap().value().contains("value"));
|
||||
/// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value");
|
||||
/// ```
|
||||
pub fn add(&mut self, mut cookie: Cookie<'static>) {
|
||||
self.sign_cookie(&mut cookie);
|
||||
self.parent.add(cookie);
|
||||
}
|
||||
|
||||
/// Adds an "original" `cookie` to this jar. The cookie's value is signed
|
||||
/// assuring integrity and authenticity. Adding an original cookie does not
|
||||
/// affect the [`CookieJar::delta()`](struct.CookieJar.html#method.delta)
|
||||
/// computation. This method is intended to be used to seed the cookie jar
|
||||
/// with cookies received from a client's HTTP message.
|
||||
///
|
||||
/// For accurate `delta` computations, this method should not be called
|
||||
/// after calling `remove`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.signed(&key).add_original(Cookie::new("name", "value"));
|
||||
///
|
||||
/// assert_eq!(jar.iter().count(), 1);
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
/// ```
|
||||
pub fn add_original(&mut self, mut cookie: Cookie<'static>) {
|
||||
self.sign_cookie(&mut cookie);
|
||||
self.parent.add_original(cookie);
|
||||
}
|
||||
|
||||
/// Signs the cookie's value assuring integrity and authenticity.
|
||||
fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
|
||||
let digest = sign(&self.key, cookie.value().as_bytes());
|
||||
let mut new_value = base64::encode(digest.as_ref());
|
||||
new_value.push_str(cookie.value());
|
||||
cookie.set_value(new_value);
|
||||
}
|
||||
|
||||
/// Removes `cookie` from the parent jar.
|
||||
///
|
||||
/// For correct removal, the passed in `cookie` must contain the same `path`
|
||||
/// and `domain` as the cookie that was initially set.
|
||||
///
|
||||
/// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more
|
||||
/// details.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// let mut signed_jar = jar.signed(&key);
|
||||
///
|
||||
/// signed_jar.add(Cookie::new("name", "value"));
|
||||
/// assert!(signed_jar.get("name").is_some());
|
||||
///
|
||||
/// signed_jar.remove(Cookie::named("name"));
|
||||
/// assert!(signed_jar.get("name").is_none());
|
||||
/// ```
|
||||
pub fn remove(&mut self, cookie: Cookie<'static>) {
|
||||
self.parent.remove(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Cookie, CookieJar, Key};
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let key = Key::generate();
|
||||
let mut jar = CookieJar::new();
|
||||
assert_simple_behaviour!(jar, jar.signed(&key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private() {
|
||||
let key = Key::generate();
|
||||
let mut jar = CookieJar::new();
|
||||
assert_secure_behaviour!(jar, jar.signed(&key));
|
||||
}
|
||||
}
|
@ -3,14 +3,14 @@ use std::io::{self, Write};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_threadpool::{run, CpuFuture};
|
||||
use actix_rt::task::{spawn_blocking, JoinHandle};
|
||||
use brotli2::write::BrotliDecoder;
|
||||
use bytes::Bytes;
|
||||
use flate2::write::{GzDecoder, ZlibDecoder};
|
||||
use futures_core::{ready, Stream};
|
||||
|
||||
use super::Writer;
|
||||
use crate::error::PayloadError;
|
||||
use crate::error::{BlockingError, PayloadError};
|
||||
use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING};
|
||||
|
||||
const INPLACE: usize = 2049;
|
||||
@ -19,7 +19,7 @@ pub struct Decoder<S> {
|
||||
decoder: Option<ContentDecoder>,
|
||||
stream: S,
|
||||
eof: bool,
|
||||
fut: Option<CpuFuture<(Option<Bytes>, ContentDecoder), io::Error>>,
|
||||
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
|
||||
}
|
||||
|
||||
impl<S> Decoder<S>
|
||||
@ -79,10 +79,8 @@ where
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
if let Some(ref mut fut) = self.fut {
|
||||
let (chunk, decoder) = match ready!(Pin::new(fut).poll(cx)) {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Poll::Ready(Some(Err(e.into()))),
|
||||
};
|
||||
let (chunk, decoder) =
|
||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||
self.decoder = Some(decoder);
|
||||
self.fut.take();
|
||||
if let Some(chunk) = chunk {
|
||||
@ -105,7 +103,7 @@ where
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
} else {
|
||||
self.fut = Some(run(move || {
|
||||
self.fut = Some(spawn_blocking(move || {
|
||||
let chunk = decoder.feed_data(chunk)?;
|
||||
Ok((chunk, decoder))
|
||||
}));
|
||||
|
@ -4,12 +4,12 @@ use std::io::{self, Write};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_threadpool::{run, CpuFuture};
|
||||
use actix_rt::task::{spawn_blocking, JoinHandle};
|
||||
use brotli2::write::BrotliEncoder;
|
||||
use bytes::Bytes;
|
||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||
use futures_core::ready;
|
||||
use pin_project::{pin_project, project};
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::body::{Body, BodySize, MessageBody, ResponseBody};
|
||||
use crate::http::header::{ContentEncoding, CONTENT_ENCODING};
|
||||
@ -17,6 +17,7 @@ use crate::http::{HeaderValue, StatusCode};
|
||||
use crate::{Error, ResponseHead};
|
||||
|
||||
use super::Writer;
|
||||
use crate::error::BlockingError;
|
||||
|
||||
const INPLACE: usize = 1024;
|
||||
|
||||
@ -26,7 +27,7 @@ pub struct Encoder<B> {
|
||||
#[pin]
|
||||
body: EncoderBody<B>,
|
||||
encoder: Option<ContentEncoder>,
|
||||
fut: Option<CpuFuture<ContentEncoder, io::Error>>,
|
||||
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
|
||||
}
|
||||
|
||||
impl<B: MessageBody> Encoder<B> {
|
||||
@ -79,7 +80,7 @@ impl<B: MessageBody> Encoder<B> {
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
#[pin_project(project = EncoderBodyProj)]
|
||||
enum EncoderBody<B> {
|
||||
Bytes(Bytes),
|
||||
Stream(#[pin] B),
|
||||
@ -95,22 +96,22 @@ impl<B: MessageBody> MessageBody for EncoderBody<B> {
|
||||
}
|
||||
}
|
||||
|
||||
#[project]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Error>>> {
|
||||
#[project]
|
||||
match self.project() {
|
||||
EncoderBody::Bytes(b) => {
|
||||
EncoderBodyProj::Bytes(b) => {
|
||||
if b.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(std::mem::take(b))))
|
||||
}
|
||||
}
|
||||
EncoderBody::Stream(b) => b.poll_next(cx),
|
||||
EncoderBody::BoxedStream(ref mut b) => Pin::new(b.as_mut()).poll_next(cx),
|
||||
EncoderBodyProj::Stream(b) => b.poll_next(cx),
|
||||
EncoderBodyProj::BoxedStream(ref mut b) => {
|
||||
Pin::new(b.as_mut()).poll_next(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -135,10 +136,8 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
||||
}
|
||||
|
||||
if let Some(ref mut fut) = this.fut {
|
||||
let mut encoder = match ready!(Pin::new(fut).poll(cx)) {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Poll::Ready(Some(Err(e.into()))),
|
||||
};
|
||||
let mut encoder =
|
||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||
let chunk = encoder.take();
|
||||
*this.encoder = Some(encoder);
|
||||
this.fut.take();
|
||||
@ -160,7 +159,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
} else {
|
||||
*this.fut = Some(run(move || {
|
||||
*this.fut = Some(spawn_blocking(move || {
|
||||
encoder.write(&chunk)?;
|
||||
Ok(encoder)
|
||||
}));
|
||||
|
@ -1,4 +1,5 @@
|
||||
//! Error and Result module
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::io::Write;
|
||||
use std::str::Utf8Error;
|
||||
@ -6,8 +7,7 @@ use std::string::FromUtf8Error;
|
||||
use std::{fmt, io, result};
|
||||
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
pub use actix_threadpool::BlockingError;
|
||||
use actix_utils::framed::DispatcherError as FramedDispatcherError;
|
||||
use actix_utils::dispatcher::DispatcherError as FramedDispatcherError;
|
||||
use actix_utils::timeout::TimeoutError;
|
||||
use bytes::BytesMut;
|
||||
use derive_more::{Display, From};
|
||||
@ -18,13 +18,12 @@ use serde::de::value::Error as DeError;
|
||||
use serde_json::error::Error as JsonError;
|
||||
use serde_urlencoded::ser::Error as FormError;
|
||||
|
||||
// re-export for convenience
|
||||
use crate::body::Body;
|
||||
pub use crate::cookie::ParseError as CookieParseError;
|
||||
use crate::helpers::Writer;
|
||||
use crate::response::{Response, ResponseBuilder};
|
||||
|
||||
/// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html)
|
||||
/// A specialized [`std::result::Result`]
|
||||
/// for actix web operations
|
||||
///
|
||||
/// This typedef is generally used to avoid writing out
|
||||
@ -99,10 +98,6 @@ impl fmt::Debug for Error {
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn cause(&self) -> Option<&dyn std::error::Error> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
None
|
||||
}
|
||||
@ -152,7 +147,10 @@ impl From<ResponseBuilder> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `GATEWAY_TIMEOUT` for `TimeoutError`
|
||||
/// Inspects the underlying enum and returns an appropriate status code.
|
||||
///
|
||||
/// If the variant is [`TimeoutError::Service`], the error code of the service is returned.
|
||||
/// Otherwise, [`StatusCode::GATEWAY_TIMEOUT`] is returned.
|
||||
impl<E: ResponseError> ResponseError for TimeoutError<E> {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
@ -166,48 +164,44 @@ impl<E: ResponseError> ResponseError for TimeoutError<E> {
|
||||
#[display(fmt = "UnknownError")]
|
||||
struct UnitError;
|
||||
|
||||
/// `InternalServerError` for `UnitError`
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`UnitError`].
|
||||
impl ResponseError for UnitError {}
|
||||
|
||||
/// `InternalServerError` for `JsonError`
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`JsonError`].
|
||||
impl ResponseError for JsonError {}
|
||||
|
||||
/// `InternalServerError` for `FormError`
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`FormError`].
|
||||
impl ResponseError for FormError {}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
/// `InternalServerError` for `openssl::ssl::Error`
|
||||
impl ResponseError for actix_connect::ssl::openssl::SslError {}
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix_tls::accept::openssl::SslError`].
|
||||
impl ResponseError for actix_tls::accept::openssl::SslError {}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
/// `InternalServerError` for `openssl::ssl::HandshakeError`
|
||||
impl<T: std::fmt::Debug> ResponseError for actix_tls::openssl::HandshakeError<T> {}
|
||||
|
||||
/// Return `BAD_REQUEST` for `de::value::Error`
|
||||
/// Returns [`StatusCode::BAD_REQUEST`] for [`DeError`].
|
||||
impl ResponseError for DeError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
/// `InternalServerError` for `Canceled`
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`Canceled`].
|
||||
impl ResponseError for Canceled {}
|
||||
|
||||
/// `InternalServerError` for `BlockingError`
|
||||
impl<E: fmt::Debug> ResponseError for BlockingError<E> {}
|
||||
|
||||
/// Return `BAD_REQUEST` for `Utf8Error`
|
||||
/// Returns [`StatusCode::BAD_REQUEST`] for [`Utf8Error`].
|
||||
impl ResponseError for Utf8Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `InternalServerError` for `HttpError`,
|
||||
/// Response generation can return `HttpError`, so it is internal error
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`HttpError`].
|
||||
impl ResponseError for HttpError {}
|
||||
|
||||
/// Return `InternalServerError` for `io::Error`
|
||||
/// Inspects the underlying [`io::ErrorKind`] and returns an appropriate status code.
|
||||
///
|
||||
/// If the error is [`io::ErrorKind::NotFound`], [`StatusCode::NOT_FOUND`] is returned. If the
|
||||
/// error is [`io::ErrorKind::PermissionDenied`], [`StatusCode::FORBIDDEN`] is returned. Otherwise,
|
||||
/// [`StatusCode::INTERNAL_SERVER_ERROR`] is returned.
|
||||
impl ResponseError for io::Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self.kind() {
|
||||
@ -218,7 +212,7 @@ impl ResponseError for io::Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// `BadRequest` for `InvalidHeaderValue`
|
||||
/// Returns [`StatusCode::BAD_REQUEST`] for [`header::InvalidHeaderValue`].
|
||||
impl ResponseError for header::InvalidHeaderValue {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
@ -307,33 +301,60 @@ impl From<httparse::Error> for ParseError {
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of errors that can occur running blocking tasks in thread pool.
|
||||
#[derive(Debug, Display)]
|
||||
#[display(fmt = "Blocking thread pool is gone")]
|
||||
pub struct BlockingError;
|
||||
|
||||
impl std::error::Error for BlockingError {}
|
||||
|
||||
/// `InternalServerError` for `BlockingError`
|
||||
impl ResponseError for BlockingError {}
|
||||
|
||||
#[derive(Display, Debug)]
|
||||
/// A set of errors that can occur during payload parsing
|
||||
pub enum PayloadError {
|
||||
/// A payload reached EOF, but is not complete.
|
||||
#[display(
|
||||
fmt = "A payload reached EOF, but is not complete. With error: {:?}",
|
||||
fmt = "A payload reached EOF, but is not complete. Inner error: {:?}",
|
||||
_0
|
||||
)]
|
||||
Incomplete(Option<io::Error>),
|
||||
/// Content encoding stream corruption
|
||||
|
||||
/// Content encoding stream corruption.
|
||||
#[display(fmt = "Can not decode content-encoding.")]
|
||||
EncodingCorrupted,
|
||||
/// A payload reached size limit.
|
||||
#[display(fmt = "A payload reached size limit.")]
|
||||
|
||||
/// Payload reached size limit.
|
||||
#[display(fmt = "Payload reached size limit.")]
|
||||
Overflow,
|
||||
/// A payload length is unknown.
|
||||
#[display(fmt = "A payload length is unknown.")]
|
||||
|
||||
/// Payload length is unknown.
|
||||
#[display(fmt = "Payload length is unknown.")]
|
||||
UnknownLength,
|
||||
/// Http2 payload error
|
||||
|
||||
/// HTTP/2 payload error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
Http2Payload(h2::Error),
|
||||
/// Io error
|
||||
|
||||
/// Generic I/O error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl std::error::Error for PayloadError {}
|
||||
impl std::error::Error for PayloadError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
PayloadError::Incomplete(None) => None,
|
||||
PayloadError::Incomplete(Some(err)) => Some(err as &dyn std::error::Error),
|
||||
PayloadError::EncodingCorrupted => None,
|
||||
PayloadError::Overflow => None,
|
||||
PayloadError::UnknownLength => None,
|
||||
PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error),
|
||||
PayloadError::Io(err) => Some(err as &dyn std::error::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<h2::Error> for PayloadError {
|
||||
fn from(err: h2::Error) -> Self {
|
||||
@ -353,15 +374,12 @@ impl From<io::Error> for PayloadError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockingError<io::Error>> for PayloadError {
|
||||
fn from(err: BlockingError<io::Error>) -> Self {
|
||||
match err {
|
||||
BlockingError::Error(e) => PayloadError::Io(e),
|
||||
BlockingError::Canceled => PayloadError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Operation is canceled",
|
||||
)),
|
||||
}
|
||||
impl From<BlockingError> for PayloadError {
|
||||
fn from(_: BlockingError) -> Self {
|
||||
PayloadError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Operation is canceled",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -452,10 +470,10 @@ impl ResponseError for ContentTypeError {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, U: Encoder + Decoder> ResponseError for FramedDispatcherError<E, U>
|
||||
impl<E, U: Encoder<I> + Decoder, I> ResponseError for FramedDispatcherError<E, U, I>
|
||||
where
|
||||
E: fmt::Debug + fmt::Display,
|
||||
<U as Encoder>::Error: fmt::Debug,
|
||||
<U as Encoder<I>>::Error: fmt::Debug,
|
||||
<U as Decoder>::Error: fmt::Debug,
|
||||
{
|
||||
}
|
||||
@ -950,21 +968,10 @@ where
|
||||
InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "actors")]
|
||||
/// `InternalServerError` for `actix::MailboxError`
|
||||
/// This is supported on feature=`actors` only
|
||||
impl ResponseError for actix::MailboxError {}
|
||||
|
||||
#[cfg(feature = "actors")]
|
||||
/// `InternalServerError` for `actix::ResolverError`
|
||||
/// This is supported on feature=`actors` only
|
||||
impl ResponseError for actix::actors::resolver::ResolverError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::{Error as HttpError, StatusCode};
|
||||
use httparse;
|
||||
use std::io;
|
||||
|
||||
#[test]
|
||||
@ -1018,22 +1025,22 @@ mod tests {
|
||||
fn test_payload_error() {
|
||||
let err: PayloadError =
|
||||
io::Error::new(io::ErrorKind::Other, "ParseError").into();
|
||||
assert!(format!("{}", err).contains("ParseError"));
|
||||
assert!(err.to_string().contains("ParseError"));
|
||||
|
||||
let err = PayloadError::Incomplete(None);
|
||||
assert_eq!(
|
||||
format!("{}", err),
|
||||
"A payload reached EOF, but is not complete. With error: None"
|
||||
err.to_string(),
|
||||
"A payload reached EOF, but is not complete. Inner error: None"
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! from {
|
||||
($from:expr => $error:pat) => {
|
||||
match ParseError::from($from) {
|
||||
e @ $error => {
|
||||
assert!(format!("{}", e).len() >= 5);
|
||||
err @ $error => {
|
||||
assert!(err.to_string().len() >= 5);
|
||||
}
|
||||
e => unreachable!("{:?}", e),
|
||||
err => unreachable!("{:?}", err),
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1076,7 +1083,7 @@ mod tests {
|
||||
let err = PayloadError::Overflow;
|
||||
let resp_err: &dyn ResponseError = &err;
|
||||
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
|
||||
assert_eq!(err.to_string(), "A payload reached size limit.");
|
||||
assert_eq!(err.to_string(), "Payload reached size limit.");
|
||||
let not_err = resp_err.downcast_ref::<ContentTypeError>();
|
||||
assert!(not_err.is_none());
|
||||
}
|
||||
|
@ -1,66 +1,133 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::fmt;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
fmt, mem,
|
||||
};
|
||||
|
||||
use fxhash::FxHashMap;
|
||||
use ahash::AHashMap;
|
||||
|
||||
/// A type map for request extensions.
|
||||
///
|
||||
/// All entries into this map must be owned types (or static references).
|
||||
#[derive(Default)]
|
||||
/// A type map of request extensions.
|
||||
pub struct Extensions {
|
||||
/// Use FxHasher with a std HashMap with for faster
|
||||
/// lookups on the small `TypeId` (u64 equivalent) keys.
|
||||
map: FxHashMap<TypeId, Box<dyn Any>>,
|
||||
map: AHashMap<TypeId, Box<dyn Any>>,
|
||||
}
|
||||
|
||||
impl Extensions {
|
||||
/// Create an empty `Extensions`.
|
||||
/// Creates an empty `Extensions`.
|
||||
#[inline]
|
||||
pub fn new() -> 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
|
||||
/// be returned.
|
||||
pub fn insert<T: 'static>(&mut self, val: T) {
|
||||
self.map.insert(TypeId::of::<T>(), Box::new(val));
|
||||
/// If an item of this type was already stored, it will be replaced and returned.
|
||||
///
|
||||
/// ```
|
||||
/// # 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 {
|
||||
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> {
|
||||
self.map
|
||||
.get(&TypeId::of::<T>())
|
||||
.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> {
|
||||
self.map
|
||||
.get_mut(&TypeId::of::<T>())
|
||||
.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> {
|
||||
self.map
|
||||
.remove(&TypeId::of::<T>())
|
||||
.and_then(|boxed| boxed.downcast().ok().map(|boxed| *boxed))
|
||||
self.map.remove(&TypeId::of::<T>()).and_then(downcast_owned)
|
||||
}
|
||||
|
||||
/// 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]
|
||||
pub fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
|
||||
/// Extends self with the items from another `Extensions`.
|
||||
pub fn extend(&mut self, other: Extensions) {
|
||||
self.map.extend(other.map);
|
||||
}
|
||||
|
||||
/// Sets (or overrides) items from `other` into this map.
|
||||
pub(crate) fn drain_from(&mut self, other: &mut Self) {
|
||||
self.map.extend(mem::take(&mut other.map));
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Extensions {
|
||||
@ -69,10 +136,14 @@ impl fmt::Debug for Extensions {
|
||||
}
|
||||
}
|
||||
|
||||
fn downcast_owned<T: 'static>(boxed: Box<dyn Any>) -> Option<T> {
|
||||
boxed.downcast().ok().map(|boxed| *boxed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_remove() {
|
||||
let mut map = Extensions::new();
|
||||
@ -178,4 +249,57 @@ mod tests {
|
||||
assert_eq!(extensions.get::<bool>(), None);
|
||||
assert_eq!(extensions.get(), Some(&MyType(10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend() {
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct MyType(i32);
|
||||
|
||||
let mut extensions = Extensions::new();
|
||||
|
||||
extensions.insert(5i32);
|
||||
extensions.insert(MyType(10));
|
||||
|
||||
let mut other = Extensions::new();
|
||||
|
||||
other.insert(15i32);
|
||||
other.insert(20u8);
|
||||
|
||||
extensions.extend(other);
|
||||
|
||||
assert_eq!(extensions.get(), Some(&15i32));
|
||||
assert_eq!(extensions.get_mut(), Some(&mut 15i32));
|
||||
|
||||
assert_eq!(extensions.remove::<i32>(), Some(15i32));
|
||||
assert!(extensions.get::<i32>().is_none());
|
||||
|
||||
assert_eq!(extensions.get::<bool>(), None);
|
||||
assert_eq!(extensions.get(), Some(&MyType(10)));
|
||||
|
||||
assert_eq!(extensions.get(), Some(&20u8));
|
||||
assert_eq!(extensions.get_mut(), Some(&mut 20u8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drain_from() {
|
||||
let mut ext = Extensions::new();
|
||||
ext.insert(2isize);
|
||||
|
||||
let mut more_ext = Extensions::new();
|
||||
|
||||
more_ext.insert(5isize);
|
||||
more_ext.insert(5usize);
|
||||
|
||||
assert_eq!(ext.get::<isize>(), Some(&2isize));
|
||||
assert_eq!(ext.get::<usize>(), None);
|
||||
assert_eq!(more_ext.get::<isize>(), Some(&5isize));
|
||||
assert_eq!(more_ext.get::<usize>(), Some(&5usize));
|
||||
|
||||
ext.drain_from(&mut more_ext);
|
||||
|
||||
assert_eq!(ext.get::<isize>(), Some(&5isize));
|
||||
assert_eq!(ext.get::<usize>(), Some(&5usize));
|
||||
assert_eq!(more_ext.get::<isize>(), None);
|
||||
assert_eq!(more_ext.get::<usize>(), None);
|
||||
}
|
||||
}
|
||||
|
@ -173,13 +173,12 @@ impl Decoder for ClientPayloadCodec {
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for ClientCodec {
|
||||
type Item = Message<(RequestHeadType, BodySize)>;
|
||||
impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(
|
||||
&mut self,
|
||||
item: Self::Item,
|
||||
item: Message<(RequestHeadType, BodySize)>,
|
||||
dst: &mut BytesMut,
|
||||
) -> Result<(), Self::Error> {
|
||||
match item {
|
||||
@ -224,15 +223,3 @@ impl Encoder for ClientCodec {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ impl Codec {
|
||||
} else {
|
||||
Flags::empty()
|
||||
};
|
||||
|
||||
Codec {
|
||||
config,
|
||||
flags,
|
||||
@ -69,26 +70,26 @@ impl Codec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if request is upgrade.
|
||||
#[inline]
|
||||
/// Check if request is upgrade
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.ctype == ConnectionType::Upgrade
|
||||
}
|
||||
|
||||
/// Check if last response is keep-alive.
|
||||
#[inline]
|
||||
/// Check if last response is keep-alive
|
||||
pub fn keepalive(&self) -> bool {
|
||||
self.ctype == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Check if keep-alive enabled on server level.
|
||||
#[inline]
|
||||
/// Check if keep-alive enabled on server level
|
||||
pub fn keepalive_enabled(&self) -> bool {
|
||||
self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
||||
}
|
||||
|
||||
/// Check last request's message type.
|
||||
#[inline]
|
||||
/// Check last request's message type
|
||||
pub fn message_type(&self) -> MessageType {
|
||||
if self.flags.contains(Flags::STREAM) {
|
||||
MessageType::Stream
|
||||
@ -110,8 +111,8 @@ impl Decoder for Codec {
|
||||
type Error = ParseError;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
if self.payload.is_some() {
|
||||
Ok(match self.payload.as_mut().unwrap().decode(src)? {
|
||||
if let Some(ref mut payload) = self.payload {
|
||||
Ok(match payload.decode(src)? {
|
||||
Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))),
|
||||
Some(PayloadItem::Eof) => {
|
||||
self.payload.take();
|
||||
@ -144,13 +145,12 @@ impl Decoder for Codec {
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for Codec {
|
||||
type Item = Message<(Response<()>, BodySize)>;
|
||||
impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(
|
||||
&mut self,
|
||||
item: Self::Item,
|
||||
item: Message<(Response<()>, BodySize)>,
|
||||
dst: &mut BytesMut,
|
||||
) -> Result<(), Self::Error> {
|
||||
match item {
|
||||
|
@ -1,7 +1,6 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::io;
|
||||
use std::marker::PhantomData;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::task::Poll;
|
||||
|
||||
use actix_codec::Decoder;
|
||||
@ -15,7 +14,7 @@ use crate::header::HeaderMap;
|
||||
use crate::message::{ConnectionType, ResponseHead};
|
||||
use crate::request::Request;
|
||||
|
||||
const MAX_BUFFER_SIZE: usize = 131_072;
|
||||
pub(crate) const MAX_BUFFER_SIZE: usize = 131_072;
|
||||
const MAX_HEADERS: usize = 96;
|
||||
|
||||
/// Incoming message decoder
|
||||
@ -46,7 +45,7 @@ impl<T: MessageType> Decoder for MessageDecoder<T> {
|
||||
|
||||
pub(crate) enum PayloadLength {
|
||||
Payload(PayloadType),
|
||||
Upgrade,
|
||||
UpgradeWebSocket,
|
||||
None,
|
||||
}
|
||||
|
||||
@ -65,7 +64,7 @@ pub(crate) trait MessageType: Sized {
|
||||
raw_headers: &[HeaderIndex],
|
||||
) -> Result<PayloadLength, ParseError> {
|
||||
let mut ka = None;
|
||||
let mut has_upgrade = false;
|
||||
let mut has_upgrade_websocket = false;
|
||||
let mut expect = false;
|
||||
let mut chunked = false;
|
||||
let mut content_length = None;
|
||||
@ -77,12 +76,14 @@ pub(crate) trait MessageType: Sized {
|
||||
let name =
|
||||
HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap();
|
||||
|
||||
// Unsafe: httparse check header value for valid utf-8
|
||||
// SAFETY: httparse already checks header value is only visible ASCII bytes
|
||||
// from_maybe_shared_unchecked contains debug assertions so they are omitted here
|
||||
let value = unsafe {
|
||||
HeaderValue::from_maybe_shared_unchecked(
|
||||
slice.slice(idx.value.0..idx.value.1),
|
||||
)
|
||||
};
|
||||
|
||||
match name {
|
||||
header::CONTENT_LENGTH => {
|
||||
if let Ok(s) = value.to_str() {
|
||||
@ -124,12 +125,9 @@ pub(crate) trait MessageType: Sized {
|
||||
};
|
||||
}
|
||||
header::UPGRADE => {
|
||||
has_upgrade = true;
|
||||
// check content-length, some clients (dart)
|
||||
// sends "content-length: 0" with websocket upgrade
|
||||
if let Ok(val) = value.to_str().map(|val| val.trim()) {
|
||||
if val.eq_ignore_ascii_case("websocket") {
|
||||
content_length = None;
|
||||
has_upgrade_websocket = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,7 +137,7 @@ pub(crate) trait MessageType: Sized {
|
||||
expect = true;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
headers.append(name, value);
|
||||
@ -156,13 +154,13 @@ pub(crate) trait MessageType: Sized {
|
||||
Ok(PayloadLength::Payload(PayloadType::Payload(
|
||||
PayloadDecoder::chunked(),
|
||||
)))
|
||||
} else if has_upgrade_websocket {
|
||||
Ok(PayloadLength::UpgradeWebSocket)
|
||||
} else if let Some(len) = content_length {
|
||||
// Content-Length
|
||||
Ok(PayloadLength::Payload(PayloadType::Payload(
|
||||
PayloadDecoder::length(len),
|
||||
)))
|
||||
} else if has_upgrade {
|
||||
Ok(PayloadLength::Upgrade)
|
||||
} else {
|
||||
Ok(PayloadLength::None)
|
||||
}
|
||||
@ -184,16 +182,11 @@ impl MessageType for Request {
|
||||
&mut self.head_mut().headers
|
||||
}
|
||||
|
||||
#[allow(clippy::uninit_assumed_init)]
|
||||
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
|
||||
// Unsafe: we read only this data only after httparse parses headers into.
|
||||
// performance bump for pipeline benchmarks.
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||
|
||||
let (len, method, uri, ver, h_len) = {
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||
|
||||
let mut req = httparse::Request::new(&mut parsed);
|
||||
match req.parse(src)? {
|
||||
@ -210,7 +203,15 @@ impl MessageType for Request {
|
||||
|
||||
(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)
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -222,16 +223,13 @@ impl MessageType for Request {
|
||||
// payload decoder
|
||||
let decoder = match length {
|
||||
PayloadLength::Payload(pl) => pl,
|
||||
PayloadLength::Upgrade => {
|
||||
PayloadLength::UpgradeWebSocket => {
|
||||
// upgrade(websocket)
|
||||
PayloadType::Stream(PayloadDecoder::eof())
|
||||
}
|
||||
PayloadLength::None => {
|
||||
if method == Method::CONNECT {
|
||||
PayloadType::Stream(PayloadDecoder::eof())
|
||||
} else if src.len() >= MAX_BUFFER_SIZE {
|
||||
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||
return Err(ParseError::TooLarge);
|
||||
} else {
|
||||
PayloadType::None
|
||||
}
|
||||
@ -260,16 +258,11 @@ impl MessageType for ResponseHead {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
#[allow(clippy::uninit_assumed_init)]
|
||||
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
|
||||
// Unsafe: we read only this data only after httparse parses headers into.
|
||||
// performance bump for pipeline benchmarks.
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||
|
||||
let (len, ver, status, h_len) = {
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||
unsafe { MaybeUninit::uninit().assume_init() };
|
||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||
|
||||
let mut res = httparse::Response::new(&mut parsed);
|
||||
match res.parse(src)? {
|
||||
@ -285,7 +278,14 @@ impl MessageType for ResponseHead {
|
||||
|
||||
(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -301,9 +301,6 @@ impl MessageType for ResponseHead {
|
||||
} else if status == StatusCode::SWITCHING_PROTOCOLS {
|
||||
// switching protocol or connect
|
||||
PayloadType::Stream(PayloadDecoder::eof())
|
||||
} else if src.len() >= MAX_BUFFER_SIZE {
|
||||
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||
return Err(ParseError::TooLarge);
|
||||
} else {
|
||||
// for HTTP/1.0 read to eof and close connection
|
||||
if msg.version == Version::HTTP_10 {
|
||||
@ -324,6 +321,17 @@ pub(crate) struct HeaderIndex {
|
||||
pub(crate) value: (usize, usize),
|
||||
}
|
||||
|
||||
pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
||||
name: (0, 0),
|
||||
value: (0, 0),
|
||||
};
|
||||
|
||||
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||
|
||||
pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
||||
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||
|
||||
impl HeaderIndex {
|
||||
pub(crate) fn record(
|
||||
bytes: &[u8],
|
||||
@ -655,10 +663,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn is_unhandled(&self) -> bool {
|
||||
match self {
|
||||
PayloadType::Stream(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, PayloadType::Stream(_))
|
||||
}
|
||||
}
|
||||
|
||||
@ -670,10 +675,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
fn eof(&self) -> bool {
|
||||
match *self {
|
||||
PayloadItem::Eof => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(*self, PayloadItem::Eof)
|
||||
}
|
||||
}
|
||||
|
||||
@ -692,7 +694,7 @@ mod tests {
|
||||
match MessageDecoder::<Request>::default().decode($e) {
|
||||
Err(err) => match err {
|
||||
ParseError::Io(_) => unreachable!("Parse error expected"),
|
||||
_ => (),
|
||||
_ => {}
|
||||
},
|
||||
_ => unreachable!("Error expected"),
|
||||
}
|
||||
@ -828,8 +830,8 @@ mod tests {
|
||||
.get_all(SET_COOKIE)
|
||||
.map(|v| v.to_str().unwrap().to_owned())
|
||||
.collect();
|
||||
assert_eq!(val[1], "c1=cookie1");
|
||||
assert_eq!(val[0], "c2=cookie2");
|
||||
assert_eq!(val[0], "c1=cookie1");
|
||||
assert_eq!(val[1], "c2=cookie2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -979,7 +981,7 @@ mod tests {
|
||||
unreachable!("Error");
|
||||
}
|
||||
|
||||
// type in chunked
|
||||
// intentional typo in "chunked"
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
transfer-encoding: chnked\r\n\r\n",
|
||||
@ -1040,7 +1042,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_upgrade() {
|
||||
fn test_http_request_upgrade_websocket() {
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
connection: upgrade\r\n\
|
||||
@ -1054,6 +1056,26 @@ mod tests {
|
||||
assert!(pl.is_unhandled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_upgrade_h2c() {
|
||||
let mut buf = BytesMut::from(
|
||||
"GET /test HTTP/1.1\r\n\
|
||||
connection: upgrade, http2-settings\r\n\
|
||||
upgrade: h2c\r\n\
|
||||
http2-settings: dummy\r\n\r\n",
|
||||
);
|
||||
let mut reader = MessageDecoder::<Request>::default();
|
||||
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||
// `connection: upgrade, http2-settings` doesn't work properly..
|
||||
// see MessageType::set_headers().
|
||||
//
|
||||
// The line below should be:
|
||||
// assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||
assert!(req.upgrade());
|
||||
assert!(!pl.is_unhandled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_request_parser_utf8() {
|
||||
let mut buf = BytesMut::from(
|
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user