mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-04 18:06:23 +02:00
Compare commits
4 Commits
files-chun
...
1.0
Author | SHA1 | Date | |
---|---|---|---|
7880d5e2bf | |||
c23020d266 | |||
1d45639bed | |||
6a672c9097 |
41
.appveyor.yml
Normal file
41
.appveyor.yml
Normal file
@ -0,0 +1,41 @@
|
||||
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"
|
@ -1,17 +0,0 @@
|
||||
[alias]
|
||||
lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
|
||||
lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo"
|
||||
|
||||
# lib checking
|
||||
ci-check-min = "hack --workspace check --no-default-features"
|
||||
ci-check-default = "hack --workspace check"
|
||||
ci-check-default-tests = "check --workspace --tests"
|
||||
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,experimental-io-uring check"
|
||||
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
|
||||
|
||||
# testing
|
||||
ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture"
|
||||
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
|
||||
|
||||
# compile docs as docs.rs would
|
||||
# RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace
|
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [robjtede]
|
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,37 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a bug report.
|
||||
---
|
||||
|
||||
Your issue may already be reported!
|
||||
Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one.
|
||||
|
||||
## Expected Behavior
|
||||
<!--- If you're describing a bug, tell us what should happen -->
|
||||
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
||||
|
||||
## Current Behavior
|
||||
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
||||
|
||||
## Possible Solution
|
||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
<!--- or ideas how to implement the addition or change -->
|
||||
|
||||
## Steps to Reproduce (for bugs)
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Context
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
|
||||
- Rust Version (I.e, output of `rustc -V`):
|
||||
- Actix Web Version:
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Actix Discord
|
||||
url: https://discord.gg/NWpN5mmg3x
|
||||
about: Actix developer discussion and community chat
|
||||
- name: GitHub Discussions
|
||||
url: https://github.com/actix/actix-web/discussions
|
||||
about: Actix Web Q&A
|
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,27 +0,0 @@
|
||||
<!-- 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 -->
|
26
.github/workflows/bench.yml
vendored
26
.github/workflows/bench.yml
vendored
@ -1,26 +0,0 @@
|
||||
name: Benchmark
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
check_benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Check benchmark
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: bench
|
||||
args: --bench=server -- --sample-size=15
|
185
.github/workflows/ci-post-merge.yml
vendored
185
.github/workflows/ci-post-merge.yml
vendored
@ -1,185 +0,0 @@
|
||||
name: CI (post-merge)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build_and_test_nightly:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||
version:
|
||||
- nightly
|
||||
|
||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||
runs-on: ${{ matrix.target.os }}
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
CARGO_INCREMENTAL: 0
|
||||
VCPKGRS_DYNAMIC: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# install OpenSSL on Windows
|
||||
# TODO: GitHub actions docs state that OpenSSL is
|
||||
# already installed on these Windows machines somewhere
|
||||
- name: Set vcpkg root
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: vcpkg install openssl:x64-windows
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hack
|
||||
|
||||
- name: check minimal
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-min }
|
||||
|
||||
- name: check default
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-default }
|
||||
|
||||
- name: tests
|
||||
timeout-minutes: 60
|
||||
run: |
|
||||
cargo test --lib --tests -p=actix-router --all-features
|
||||
cargo test --lib --tests -p=actix-http --all-features
|
||||
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||
cargo test --lib --tests -p=awc --all-features
|
||||
cargo test --lib --tests -p=actix-http-test --all-features
|
||||
cargo test --lib --tests -p=actix-test --all-features
|
||||
cargo test --lib --tests -p=actix-files
|
||||
cargo test --lib --tests -p=actix-multipart --all-features
|
||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
|
||||
- name: tests (io-uring)
|
||||
if: matrix.target.os == 'ubuntu-latest'
|
||||
timeout-minutes: 60
|
||||
run: >
|
||||
sudo bash -c "ulimit -Sl 512
|
||||
&& ulimit -Hl 512
|
||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
ci_feature_powerset_check:
|
||||
name: Verify Feature Combinations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hack
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset }
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset-linux }
|
||||
|
||||
# job currently (1st Feb 2022) segfaults
|
||||
# coverage:
|
||||
# name: coverage
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
|
||||
# - name: Install stable
|
||||
# uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# toolchain: stable-x86_64-unknown-linux-gnu
|
||||
# profile: minimal
|
||||
# override: true
|
||||
|
||||
# - name: Generate Cargo.lock
|
||||
# uses: actions-rs/cargo@v1
|
||||
# with: { command: generate-lockfile }
|
||||
# - name: Cache Dependencies
|
||||
# uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
# - name: Generate coverage file
|
||||
# run: |
|
||||
# cargo install cargo-tarpaulin --vers "^0.13"
|
||||
# cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
||||
# - name: Upload to Codecov
|
||||
# uses: codecov/codecov-action@v1
|
||||
# with: { file: cobertura.xml }
|
||||
|
||||
nextest:
|
||||
name: nextest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
|
||||
- name: Install cargo-nextest
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-nextest
|
||||
|
||||
- name: Test with cargo-nextest
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: nextest
|
||||
args: run
|
120
.github/workflows/ci.yml
vendored
120
.github/workflows/ci.yml
vendored
@ -1,120 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||
version:
|
||||
- 1.54.0 # MSRV
|
||||
- stable
|
||||
|
||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||
runs-on: ${{ matrix.target.os }}
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
CARGO_INCREMENTAL: 0
|
||||
VCPKGRS_DYNAMIC: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# install OpenSSL on Windows
|
||||
# TODO: GitHub actions docs state that OpenSSL is
|
||||
# already installed on these Windows machines somewhere
|
||||
- name: Set vcpkg root
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: vcpkg install openssl:x64-windows
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hack
|
||||
|
||||
- name: check minimal
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-min }
|
||||
|
||||
- name: check default
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-default }
|
||||
|
||||
- name: tests
|
||||
timeout-minutes: 60
|
||||
run: |
|
||||
cargo test --lib --tests -p=actix-router --all-features
|
||||
cargo test --lib --tests -p=actix-http --all-features
|
||||
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||
cargo test --lib --tests -p=awc --all-features
|
||||
cargo test --lib --tests -p=actix-http-test --all-features
|
||||
cargo test --lib --tests -p=actix-test --all-features
|
||||
cargo test --lib --tests -p=actix-files
|
||||
cargo test --lib --tests -p=actix-multipart --all-features
|
||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
|
||||
- name: tests (io-uring)
|
||||
if: matrix.target.os == 'ubuntu-latest'
|
||||
timeout-minutes: 60
|
||||
run: >
|
||||
sudo bash -c "ulimit -Sl 512
|
||||
&& ulimit -Hl 512
|
||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
rustdoc:
|
||||
name: doc tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
|
||||
- name: doc tests
|
||||
uses: actions-rs/cargo@v1
|
||||
timeout-minutes: 60
|
||||
with: { command: ci-doctest }
|
66
.github/workflows/clippy-fmt.yml
vendored
66
.github/workflows/clippy-fmt.yml
vendored
@ -1,66 +0,0 @@
|
||||
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
|
||||
profile: minimal
|
||||
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
|
||||
profile: minimal
|
||||
components: clippy
|
||||
override: true
|
||||
|
||||
- name: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: generate-lockfile }
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: Check with Clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --workspace --tests --examples --all-features
|
||||
|
||||
lint-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rust-docs
|
||||
- name: Check for broken intra-doc links
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
with:
|
||||
command: doc
|
||||
args: --no-deps --all-features --workspace
|
35
.github/workflows/upload-doc.yml
vendored
35
.github/workflows/upload-doc.yml
vendored
@ -1,35 +0,0 @@
|
||||
name: Upload Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly-x86_64-unknown-linux-gnu
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Build Docs
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --workspace --all-features --no-deps
|
||||
|
||||
- name: Tweak HTML
|
||||
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
|
||||
|
||||
- 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
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -9,13 +9,6 @@ guide/build/
|
||||
*.pid
|
||||
*.sock
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Configuration directory generated by CLion
|
||||
.idea
|
||||
|
||||
# Configuration directory generated by VSCode
|
||||
.vscode
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"proseWrap": "never"
|
||||
}
|
58
.travis.yml
Normal file
58
.travis.yml
Normal file
@ -0,0 +1,58 @@
|
||||
language: rust
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
cache:
|
||||
# cargo: true
|
||||
apt: true
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rust: stable
|
||||
- rust: beta
|
||||
- rust: nightly-2019-08-10
|
||||
allow_failures:
|
||||
- rust: nightly-2019-08-10
|
||||
|
||||
env:
|
||||
global:
|
||||
# - RUSTFLAGS="-C link-dead-code"
|
||||
- OPENSSL_VERSION=openssl-1.0.2
|
||||
|
||||
before_install:
|
||||
- sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev
|
||||
|
||||
before_cache: |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-08-10" ]]; then
|
||||
RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install --version 0.6.11 cargo-tarpaulin
|
||||
fi
|
||||
|
||||
# Add clippy
|
||||
before_script:
|
||||
- export PATH=$PATH:~/.cargo/bin
|
||||
|
||||
script:
|
||||
- cargo update
|
||||
- cargo check --all --no-default-features
|
||||
- cargo test --all-features --all -- --nocapture
|
||||
- cd actix-http; cargo test --no-default-features --features="rust-tls" -- --nocapture; cd ..
|
||||
- cd awc; cargo test --no-default-features --features="rust-tls" -- --nocapture; cd ..
|
||||
|
||||
# Upload docs
|
||||
after_success:
|
||||
- |
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then
|
||||
cargo doc --no-deps --all-features &&
|
||||
echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html &&
|
||||
git clone https://github.com/davisp/ghp-import.git &&
|
||||
./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc &&
|
||||
echo "Uploaded documentation"
|
||||
fi
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-08-10" ]]; then
|
||||
taskset -c 0 cargo tarpaulin --out Xml --all --all-features
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
echo "Uploaded code coverage"
|
||||
fi
|
349
CHANGES.md
349
CHANGES.md
@ -1,5 +1,348 @@
|
||||
# Changelog
|
||||
# Changes
|
||||
|
||||
Changelogs are kept separately for each crate in this repo.
|
||||
## [1.0.9] - 2019-11-14
|
||||
|
||||
Actix Web changelog [is now here →](./actix-web/CHANGES.md).
|
||||
### Added
|
||||
|
||||
* Add `Payload::into_inner` method and make stored `def::Payload` public. (#1110)
|
||||
|
||||
### Changed
|
||||
|
||||
* Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129)
|
||||
|
||||
|
||||
## [1.0.8] - 2019-09-25
|
||||
|
||||
### Added
|
||||
|
||||
* Add `Scope::register_data` and `Resource::register_data` methods, parallel to
|
||||
`App::register_data`.
|
||||
|
||||
* Add `middleware::Condition` that conditionally enables another middleware
|
||||
|
||||
* Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload`
|
||||
|
||||
* Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path,
|
||||
which is useful for example with systemd.
|
||||
|
||||
### Changed
|
||||
|
||||
* Make UrlEncodedError::Overflow more informativve
|
||||
|
||||
* Use actix-testing for testing utils
|
||||
|
||||
|
||||
## [1.0.7] - 2019-08-29
|
||||
|
||||
### Fixed
|
||||
|
||||
* Request Extensions leak #1062
|
||||
|
||||
|
||||
## [1.0.6] - 2019-08-28
|
||||
|
||||
### Added
|
||||
|
||||
* Re-implement Host predicate (#989)
|
||||
|
||||
* Form immplements Responder, returning a `application/x-www-form-urlencoded` response
|
||||
|
||||
* Add `into_inner` to `Data`
|
||||
|
||||
* Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set
|
||||
the header in test requests.
|
||||
|
||||
### Changed
|
||||
|
||||
* `Query` payload made `pub`. Allows user to pattern-match the payload.
|
||||
|
||||
* Enable `rust-tls` feature for client #1045
|
||||
|
||||
* Update serde_urlencoded to 0.6.1
|
||||
|
||||
* Update url to 2.1
|
||||
|
||||
|
||||
## [1.0.5] - 2019-07-18
|
||||
|
||||
### Added
|
||||
|
||||
* Unix domain sockets (HttpServer::bind_uds) #92
|
||||
|
||||
* Actix now logs errors resulting in "internal server error" responses always, with the `error`
|
||||
logging level
|
||||
|
||||
### Fixed
|
||||
|
||||
* Restored logging of errors through the `Logger` middleware
|
||||
|
||||
|
||||
## [1.0.4] - 2019-07-17
|
||||
|
||||
### Added
|
||||
|
||||
* Add `Responder` impl for `(T, StatusCode) where T: Responder`
|
||||
|
||||
* Allow to access app's resource map via
|
||||
`ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods.
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade `rand` dependency version to 0.7
|
||||
|
||||
|
||||
## [1.0.3] - 2019-06-28
|
||||
|
||||
### Added
|
||||
|
||||
* Support asynchronous data factories #850
|
||||
|
||||
### Changed
|
||||
|
||||
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
||||
|
||||
|
||||
## [1.0.2] - 2019-06-17
|
||||
|
||||
### Changed
|
||||
|
||||
* Move cors middleware to `actix-cors` crate.
|
||||
|
||||
* Move identity middleware to `actix-identity` crate.
|
||||
|
||||
|
||||
## [1.0.1] - 2019-06-17
|
||||
|
||||
### Added
|
||||
|
||||
* Add support for PathConfig #903
|
||||
|
||||
* Add `middleware::identity::RequestIdentity` trait to `get_identity` from `HttpMessage`.
|
||||
|
||||
### Changed
|
||||
|
||||
* Move cors middleware to `actix-cors` crate.
|
||||
|
||||
* Move identity middleware to `actix-identity` crate.
|
||||
|
||||
* Disable default feature `secure-cookies`.
|
||||
|
||||
* Allow to test an app that uses async actors #897
|
||||
|
||||
* Re-apply patch from #637 #894
|
||||
|
||||
### Fixed
|
||||
|
||||
* HttpRequest::url_for is broken with nested scopes #915
|
||||
|
||||
|
||||
## [1.0.0] - 2019-06-05
|
||||
|
||||
### Added
|
||||
|
||||
* Add `Scope::configure()` method.
|
||||
|
||||
* Add `ServiceRequest::set_payload()` method.
|
||||
|
||||
* Add `test::TestRequest::set_json()` convenience method to automatically
|
||||
serialize data and set header in test requests.
|
||||
|
||||
* Add macros for head, options, trace, connect and patch http methods
|
||||
|
||||
### Changed
|
||||
|
||||
* Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix Logger request time format, and use rfc3339. #867
|
||||
|
||||
* Clear http requests pool on app service drop #860
|
||||
|
||||
|
||||
## [1.0.0-rc] - 2019-05-18
|
||||
|
||||
### Add
|
||||
|
||||
* Add `Query<T>::from_query()` to extract parameters from a query string. #846
|
||||
* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
|
||||
|
||||
### Changed
|
||||
|
||||
* `JsonConfig` is now `Send + Sync`, this implies that `error_handler` must be `Send + Sync` too.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Codegen with parameters in the path only resolves the first registered endpoint #841
|
||||
|
||||
|
||||
## [1.0.0-beta.4] - 2019-05-12
|
||||
|
||||
### Add
|
||||
|
||||
* Allow to set/override app data on scope level
|
||||
|
||||
### Changed
|
||||
|
||||
* `App::configure` take an `FnOnce` instead of `Fn`
|
||||
* Upgrade actix-net crates
|
||||
|
||||
|
||||
## [1.0.0-beta.3] - 2019-05-04
|
||||
|
||||
### Added
|
||||
|
||||
* Add helper function for executing futures `test::block_fn()`
|
||||
|
||||
### Changed
|
||||
|
||||
* Extractor configuration could be registered with `App::data()`
|
||||
or with `Resource::data()` #775
|
||||
|
||||
* Route data is unified with app data, `Route::data()` moved to resource
|
||||
level to `Resource::data()`
|
||||
|
||||
* CORS handling without headers #702
|
||||
|
||||
* Allow to construct `Data` instances to avoid double `Arc` for `Send + Sync` types.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix `NormalizePath` middleware impl #806
|
||||
|
||||
### Deleted
|
||||
|
||||
* `App::data_factory()` is deleted.
|
||||
|
||||
|
||||
## [1.0.0-beta.2] - 2019-04-24
|
||||
|
||||
### Added
|
||||
|
||||
* Add raw services support via `web::service()`
|
||||
|
||||
* Add helper functions for reading response body `test::read_body()`
|
||||
|
||||
* Add support for `remainder match` (i.e "/path/{tail}*")
|
||||
|
||||
* Extend `Responder` trait, allow to override status code and headers.
|
||||
|
||||
* Store visit and login timestamp in the identity cookie #502
|
||||
|
||||
### Changed
|
||||
|
||||
* `.to_async()` handler can return `Responder` type #792
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix async web::Data factory handling
|
||||
|
||||
|
||||
## [1.0.0-beta.1] - 2019-04-20
|
||||
|
||||
### Added
|
||||
|
||||
* Add helper functions for reading test response body,
|
||||
`test::read_response()` and test::read_response_json()`
|
||||
|
||||
* Add `.peer_addr()` #744
|
||||
|
||||
* Add `NormalizePath` middleware
|
||||
|
||||
### Changed
|
||||
|
||||
* Rename `RouterConfig` to `ServiceConfig`
|
||||
|
||||
* Rename `test::call_success` to `test::call_service`
|
||||
|
||||
* Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts.
|
||||
|
||||
* `CookieIdentityPolicy::max_age()` accepts value in seconds
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed `TestRequest::app_data()`
|
||||
|
||||
|
||||
## [1.0.0-alpha.6] - 2019-04-14
|
||||
|
||||
### Changed
|
||||
|
||||
* Allow to use any service as default service.
|
||||
|
||||
* Remove generic type for request payload, always use default.
|
||||
|
||||
* Removed `Decompress` middleware. Bytes, String, Json, Form extractors
|
||||
automatically decompress payload.
|
||||
|
||||
* Make extractor config type explicit. Add `FromRequest::Config` associated type.
|
||||
|
||||
|
||||
## [1.0.0-alpha.5] - 2019-04-12
|
||||
|
||||
### Added
|
||||
|
||||
* Added async io `TestBuffer` for testing.
|
||||
|
||||
### Deleted
|
||||
|
||||
* Removed native-tls support
|
||||
|
||||
|
||||
## [1.0.0-alpha.4] - 2019-04-08
|
||||
|
||||
### Added
|
||||
|
||||
* `App::configure()` allow to offload app configuration to different methods
|
||||
|
||||
* Added `URLPath` option for logger
|
||||
|
||||
* Added `ServiceRequest::app_data()`, returns `Data<T>`
|
||||
|
||||
* Added `ServiceFromRequest::app_data()`, returns `Data<T>`
|
||||
|
||||
### Changed
|
||||
|
||||
* `FromRequest` trait refactoring
|
||||
|
||||
* Move multipart support to actix-multipart crate
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix body propagation in Response::from_error. #760
|
||||
|
||||
|
||||
## [1.0.0-alpha.3] - 2019-04-02
|
||||
|
||||
### Changed
|
||||
|
||||
* Renamed `TestRequest::to_service()` to `TestRequest::to_srv_request()`
|
||||
|
||||
* Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()`
|
||||
|
||||
* Removed `Deref` impls
|
||||
|
||||
### Removed
|
||||
|
||||
* Removed unused `actix_web::web::md()`
|
||||
|
||||
|
||||
## [1.0.0-alpha.2] - 2019-03-29
|
||||
|
||||
### Added
|
||||
|
||||
* rustls support
|
||||
|
||||
### Changed
|
||||
|
||||
* use forked cookie
|
||||
|
||||
* multipart::Field renamed to MultipartField
|
||||
|
||||
## [1.0.0-alpha.1] - 2019-03-28
|
||||
|
||||
### Changed
|
||||
|
||||
* Complete architecture re-design.
|
||||
|
||||
* Return 405 response if no matching route found within resource #538
|
||||
|
@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo
|
||||
|
||||
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
|
||||
* 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
|
||||
* 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
|
||||
|
||||
@ -34,13 +34,10 @@ 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 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.
|
||||
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.
|
||||
|
||||
[@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]
|
||||
|
149
Cargo.toml
149
Cargo.toml
@ -1,21 +1,120 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "1.0.9"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
documentation = "https://docs.rs/actix-web/"
|
||||
categories = ["network-programming", "asynchronous",
|
||||
"web-programming::http-server",
|
||||
"web-programming::websocket"]
|
||||
license = "MIT/Apache-2.0"
|
||||
exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"]
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["ssl", "brotli", "flate2-zlib", "secure-cookies", "client", "rust-tls", "uds"]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "actix/actix-web", branch = "master" }
|
||||
codecov = { repository = "actix/actix-web", branch = "master", service = "github" }
|
||||
|
||||
[lib]
|
||||
name = "actix_web"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"actix-files",
|
||||
"actix-http-test",
|
||||
"actix-http",
|
||||
"actix-multipart",
|
||||
"actix-router",
|
||||
"actix-test",
|
||||
"actix-web-actors",
|
||||
"actix-web-codegen",
|
||||
"actix-web",
|
||||
"awc",
|
||||
".",
|
||||
"awc",
|
||||
"actix-http",
|
||||
"actix-cors",
|
||||
"actix-files",
|
||||
"actix-framed",
|
||||
"actix-session",
|
||||
"actix-identity",
|
||||
"actix-multipart",
|
||||
"actix-web-actors",
|
||||
"actix-web-codegen",
|
||||
"test-server",
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
|
||||
debug = 0
|
||||
[features]
|
||||
default = ["brotli", "flate2-zlib", "client", "fail"]
|
||||
|
||||
# http client
|
||||
client = ["awc"]
|
||||
|
||||
# brotli encoding, requires c compiler
|
||||
brotli = ["actix-http/brotli"]
|
||||
|
||||
# miniz-sys backend for flate2 crate
|
||||
flate2-zlib = ["actix-http/flate2-zlib"]
|
||||
|
||||
# rust backend for flate2 crate
|
||||
flate2-rust = ["actix-http/flate2-rust"]
|
||||
|
||||
# sessions feature, session require "ring" crate and c compiler
|
||||
secure-cookies = ["actix-http/secure-cookies"]
|
||||
|
||||
fail = ["actix-http/fail"]
|
||||
|
||||
# openssl
|
||||
ssl = ["openssl", "actix-server/ssl", "awc/ssl"]
|
||||
|
||||
# rustls
|
||||
rust-tls = ["rustls", "actix-server/rust-tls", "awc/rust-tls"]
|
||||
|
||||
# unix domain sockets support
|
||||
uds = ["actix-server/uds"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.1.2"
|
||||
actix-service = "0.4.1"
|
||||
actix-utils = "0.4.4"
|
||||
actix-router = "0.1.5"
|
||||
actix-rt = "0.2.4"
|
||||
actix-web-codegen = "0.1.2"
|
||||
actix-http = "0.2.11"
|
||||
actix-server = "0.6.1"
|
||||
actix-server-config = "0.1.2"
|
||||
actix-testing = "0.1.0"
|
||||
actix-threadpool = "0.1.1"
|
||||
awc = { version = "0.2.7", optional = true }
|
||||
|
||||
bytes = "0.4"
|
||||
derive_more = "0.15.0"
|
||||
encoding_rs = "0.8"
|
||||
futures = "0.1.25"
|
||||
hashbrown = "0.6.3"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
net2 = "0.2.33"
|
||||
parking_lot = "0.9"
|
||||
regex = "1.0"
|
||||
serde = { version = "1.0", features=["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.6.1"
|
||||
time = "0.1.42"
|
||||
url = "2.1"
|
||||
|
||||
# ssl support
|
||||
openssl = { version="0.10", optional = true }
|
||||
rustls = { version = "0.15", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix = "0.8.3"
|
||||
actix-connect = "0.2.2"
|
||||
actix-http-test = "0.2.4"
|
||||
rand = "0.7"
|
||||
env_logger = "0.6"
|
||||
serde_derive = "1.0"
|
||||
tokio-timer = "0.2.8"
|
||||
brotli2 = "0.3.2"
|
||||
flate2 = "1.0.2"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
@ -23,22 +122,12 @@ opt-level = 3
|
||||
codegen-units = 1
|
||||
|
||||
[patch.crates-io]
|
||||
actix-files = { path = "actix-files" }
|
||||
actix-web = { path = "." }
|
||||
actix-http = { path = "actix-http" }
|
||||
actix-http-test = { path = "actix-http-test" }
|
||||
actix-multipart = { path = "actix-multipart" }
|
||||
actix-router = { path = "actix-router" }
|
||||
actix-test = { path = "actix-test" }
|
||||
actix-web = { path = "actix-web" }
|
||||
actix-web-actors = { path = "actix-web-actors" }
|
||||
actix-http-test = { path = "test-server" }
|
||||
actix-web-codegen = { path = "actix-web-codegen" }
|
||||
actix-web-actors = { path = "actix-web-actors" }
|
||||
actix-session = { path = "actix-session" }
|
||||
actix-files = { path = "actix-files" }
|
||||
actix-multipart = { path = "actix-multipart" }
|
||||
awc = { path = "awc" }
|
||||
|
||||
# uncomment for quick testing against local actix-net repo
|
||||
# actix-service = { path = "../actix-net/actix-service" }
|
||||
# actix-macros = { path = "../actix-net/actix-macros" }
|
||||
# actix-rt = { path = "../actix-net/actix-rt" }
|
||||
# actix-codec = { path = "../actix-net/actix-codec" }
|
||||
# actix-utils = { path = "../actix-net/actix-utils" }
|
||||
# actix-tls = { path = "../actix-net/actix-tls" }
|
||||
# actix-server = { path = "../actix-net/actix-server" }
|
||||
|
@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-NOW Actix Team
|
||||
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.
|
||||
|
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017-NOW Actix Team
|
||||
Copyright (c) 2017 Nikolay Kim
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
547
MIGRATION.md
Normal file
547
MIGRATION.md
Normal file
@ -0,0 +1,547 @@
|
||||
## 1.0.1
|
||||
|
||||
* Cors middleware has been moved to `actix-cors` crate
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
use actix_web::middleware::cors::Cors;
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
use actix_cors::Cors;
|
||||
```
|
||||
|
||||
* Identity middleware has been moved to `actix-identity` crate
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
use actix_web::middleware::identity::{Identity, CookieIdentityPolicy, IdentityService};
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
use actix_identity::{Identity, CookieIdentityPolicy, IdentityService};
|
||||
```
|
||||
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
|
||||
#[derive(Default)]
|
||||
struct ExtractorConfig {
|
||||
config: String,
|
||||
}
|
||||
|
||||
impl FromRequest for YourExtractor {
|
||||
type Config = ExtractorConfig;
|
||||
type Result = Result<YourExtractor, Error>;
|
||||
|
||||
fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result {
|
||||
println!("use the config: {:?}", cfg.config);
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
App::new().resource("/route_with_config", |r| {
|
||||
r.post().with_config(handler_fn, |cfg| {
|
||||
cfg.0.config = "test".to_string();
|
||||
})
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
use the HttpRequest to get the configuration like any other `Data` with `req.app_data::<C>()` and set it with the `data()` method on the `resource`
|
||||
|
||||
```rust
|
||||
#[derive(Default)]
|
||||
struct ExtractorConfig {
|
||||
config: String,
|
||||
}
|
||||
|
||||
impl FromRequest for YourExtractor {
|
||||
type Error = Error;
|
||||
type Future = Result<Self, Self::Error>;
|
||||
type Config = ExtractorConfig;
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let cfg = req.app_data::<ExtractorConfig>();
|
||||
println!("config data?: {:?}", cfg.unwrap().role);
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
App::new().service(
|
||||
resource("/route_with_config")
|
||||
.data(ExtractorConfig {
|
||||
config: "test".to_string(),
|
||||
})
|
||||
.route(post().to(handler_fn)),
|
||||
)
|
||||
```
|
||||
|
||||
* Resource registration. 1.0 version uses generalized resource
|
||||
registration via `.service()` method.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
App.new().resource("/welcome", |r| r.f(welcome))
|
||||
```
|
||||
|
||||
use App's or Scope's `.service()` method. `.service()` method accepts
|
||||
object that implements `HttpServiceFactory` trait. By default
|
||||
actix-web provides `Resource` and `Scope` services.
|
||||
|
||||
```rust
|
||||
App.new().service(
|
||||
web::resource("/welcome")
|
||||
.route(web::get().to(welcome))
|
||||
.route(web::post().to(post_handler))
|
||||
```
|
||||
|
||||
* Scope registration.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
let app = App::new().scope("/{project_id}", |scope| {
|
||||
scope
|
||||
.resource("/path1", |r| r.f(|_| HttpResponse::Ok()))
|
||||
.resource("/path2", |r| r.f(|_| HttpResponse::Ok()))
|
||||
.resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed()))
|
||||
});
|
||||
```
|
||||
|
||||
use `.service()` for registration and `web::scope()` as scope object factory.
|
||||
|
||||
```rust
|
||||
let app = App::new().service(
|
||||
web::scope("/{project_id}")
|
||||
.service(web::resource("/path1").to(|| HttpResponse::Ok()))
|
||||
.service(web::resource("/path2").to(|| HttpResponse::Ok()))
|
||||
.service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed()))
|
||||
);
|
||||
```
|
||||
|
||||
* `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
App.new().resource("/welcome", |r| r.with(welcome))
|
||||
```
|
||||
|
||||
use `.to()` or `.to_async()` methods
|
||||
|
||||
```rust
|
||||
App.new().service(web::resource("/welcome").to(welcome))
|
||||
```
|
||||
|
||||
* Passing arguments to handler with extractors, multiple arguments are allowed
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn welcome((body, req): (Bytes, HttpRequest)) -> ... {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
use multiple arguments
|
||||
|
||||
```rust
|
||||
fn welcome(body: Bytes, req: HttpRequest) -> ... {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
* `.f()`, `.a()` and `.h()` handler registration methods have been removed.
|
||||
Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
|
||||
must use extractors.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
App.new().resource("/welcome", |r| r.f(welcome))
|
||||
```
|
||||
|
||||
use App's `to()` or `to_async()` methods
|
||||
|
||||
```rust
|
||||
App.new().service(web::resource("/welcome").to(welcome))
|
||||
```
|
||||
|
||||
* `HttpRequest` does not provide access to request's payload stream.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn index(req: &HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req
|
||||
.payload()
|
||||
.from_err()
|
||||
.fold((), |_, chunk| {
|
||||
...
|
||||
})
|
||||
.map(|_| HttpResponse::Ok().finish())
|
||||
.responder()
|
||||
}
|
||||
```
|
||||
|
||||
use `Payload` extractor
|
||||
|
||||
```rust
|
||||
fn index(stream: web::Payload) -> impl Future<Item=HttpResponse, Error=Error> {
|
||||
stream
|
||||
.from_err()
|
||||
.fold((), |_, chunk| {
|
||||
...
|
||||
})
|
||||
.map(|_| HttpResponse::Ok().finish())
|
||||
}
|
||||
```
|
||||
|
||||
* `State` is now `Data`. You register Data during the App initialization process
|
||||
and then access it from handlers either using a Data extractor or using
|
||||
HttpRequest's api.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
App.with_state(T)
|
||||
```
|
||||
|
||||
use App's `data` method
|
||||
|
||||
```rust
|
||||
App.new()
|
||||
.data(T)
|
||||
```
|
||||
|
||||
and either use the Data extractor within your handler
|
||||
|
||||
```rust
|
||||
use actix_web::web::Data;
|
||||
|
||||
fn endpoint_handler(Data<T>)){
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
.. or access your Data element from the HttpRequest
|
||||
|
||||
```rust
|
||||
fn endpoint_handler(req: HttpRequest) {
|
||||
let data: Option<Data<T>> = req.app_data::<T>();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
* AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
use actix_web::AsyncResponder;
|
||||
|
||||
fn endpoint_handler(...) -> impl Future<Item=HttpResponse, Error=Error>{
|
||||
...
|
||||
.responder()
|
||||
}
|
||||
```
|
||||
|
||||
.. simply omit AsyncResponder and the corresponding responder() finish method
|
||||
|
||||
|
||||
* Middleware
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
let app = App::new()
|
||||
.middleware(middleware::Logger::default())
|
||||
```
|
||||
|
||||
use `.wrap()` method
|
||||
|
||||
```rust
|
||||
let app = App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.route("/index.html", web::get().to(index));
|
||||
```
|
||||
|
||||
* `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
|
||||
method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn index(req: &HttpRequest) -> Responder {
|
||||
req.body()
|
||||
.and_then(|body| {
|
||||
...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
fn index(body: Bytes) -> Responder {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
* `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type
|
||||
|
||||
* StaticFiles and NamedFile has been move to separate create.
|
||||
|
||||
instead of `use actix_web::fs::StaticFile`
|
||||
|
||||
use `use actix_files::Files`
|
||||
|
||||
instead of `use actix_web::fs::Namedfile`
|
||||
|
||||
use `use actix_files::NamedFile`
|
||||
|
||||
* Multipart has been move to separate create.
|
||||
|
||||
instead of `use actix_web::multipart::Multipart`
|
||||
|
||||
use `use actix_multipart::Multipart`
|
||||
|
||||
* Response compression is not enabled by default.
|
||||
To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
|
||||
|
||||
* Session middleware moved to actix-session crate
|
||||
|
||||
* Actors support have been moved to `actix-web-actors` crate
|
||||
|
||||
* Custom Error
|
||||
|
||||
Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller.
|
||||
|
||||
Simplest migration from 0.7 to 1.0 shall include below method to the custom implementation of ResponseError:
|
||||
|
||||
```rust
|
||||
fn render_response(&self) -> HttpResponse {
|
||||
self.error_response()
|
||||
}
|
||||
```
|
||||
|
||||
## 0.7.15
|
||||
|
||||
* The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
|
||||
your routes, you should use `%20`.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let app = App::new().resource("/my index", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with(index);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let app = App::new().resource("/my%20index", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with(index);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
* If you used `AsyncResult::async` you need to replace it with `AsyncResult::future`
|
||||
|
||||
|
||||
## 0.7.4
|
||||
|
||||
* `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
|
||||
even for handler with one parameter.
|
||||
|
||||
|
||||
## 0.7
|
||||
|
||||
* `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
|
||||
use `HttpMessage::payload()` method.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn index(req: HttpRequest) -> impl Responder {
|
||||
req
|
||||
.from_err()
|
||||
.fold(...)
|
||||
....
|
||||
}
|
||||
```
|
||||
|
||||
use `.payload()`
|
||||
|
||||
```rust
|
||||
fn index(req: HttpRequest) -> impl Responder {
|
||||
req
|
||||
.payload() // <- get request payload stream
|
||||
.from_err()
|
||||
.fold(...)
|
||||
....
|
||||
}
|
||||
```
|
||||
|
||||
* [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
|
||||
trait uses `&HttpRequest` instead of `&mut HttpRequest`.
|
||||
|
||||
* Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn index(query: Query<..>, info: Json<MyStruct) -> impl Responder {}
|
||||
```
|
||||
|
||||
use tuple of extractors and use `.with()` for registration:
|
||||
|
||||
```rust
|
||||
fn index((query, json): (Query<..>, Json<MyStruct)) -> impl Responder {}
|
||||
```
|
||||
|
||||
* `Handler::handle()` uses `&self` instead of `&mut self`
|
||||
|
||||
* `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
|
||||
|
||||
* Removed deprecated `HttpServer::threads()`, use
|
||||
[HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
|
||||
|
||||
* Renamed `client::ClientConnectorError::Connector` to
|
||||
`client::ClientConnectorError::Resolver`
|
||||
|
||||
* `Route::with()` does not return `ExtractorConfig`, to configure
|
||||
extractor use `Route::with_config()`
|
||||
|
||||
instead of
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let app = App::new().resource("/index.html", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with(index)
|
||||
.limit(4096); // <- limit size of the payload
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
use
|
||||
|
||||
```rust
|
||||
|
||||
fn main() {
|
||||
let app = App::new().resource("/index.html", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with_config(index, |cfg| { // <- register handler
|
||||
cfg.limit(4096); // <- limit size of the payload
|
||||
})
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
* `Route::with_async()` does not return `ExtractorConfig`, to configure
|
||||
extractor use `Route::with_async_config()`
|
||||
|
||||
|
||||
## 0.6
|
||||
|
||||
* `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
|
||||
|
||||
* `ws::Message::Close` now includes optional close reason.
|
||||
`ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
|
||||
|
||||
* `HttpServer::threads()` renamed to `HttpServer::workers()`.
|
||||
|
||||
* `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
|
||||
Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
|
||||
|
||||
* `HttpRequest::extensions()` returns read only reference to the request's Extension
|
||||
`HttpRequest::extensions_mut()` returns mutable reference.
|
||||
|
||||
* Instead of
|
||||
|
||||
`use actix_web::middleware::{
|
||||
CookieSessionBackend, CookieSessionError, RequestSession,
|
||||
Session, SessionBackend, SessionImpl, SessionStorage};`
|
||||
|
||||
use `actix_web::middleware::session`
|
||||
|
||||
`use actix_web::middleware::session{CookieSessionBackend, CookieSessionError,
|
||||
RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};`
|
||||
|
||||
* `FromRequest::from_request()` accepts mutable reference to a request
|
||||
|
||||
* `FromRequest::Result` has to implement `Into<Reply<Self>>`
|
||||
|
||||
* [`Responder::respond_to()`](
|
||||
https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
|
||||
is generic over `S`
|
||||
|
||||
* Use `Query` extractor instead of HttpRequest::query()`.
|
||||
|
||||
```rust
|
||||
fn index(q: Query<HashMap<String, String>>) -> Result<..> {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```rust
|
||||
let q = Query::<HashMap<String, String>>::extract(req);
|
||||
```
|
||||
|
||||
* Websocket operations are implemented as `WsWriter` trait.
|
||||
you need to use `use actix_web::ws::WsWriter`
|
||||
|
||||
|
||||
## 0.5
|
||||
|
||||
* `HttpResponseBuilder::body()`, `.finish()`, `.json()`
|
||||
methods return `HttpResponse` instead of `Result<HttpResponse>`
|
||||
|
||||
* `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
|
||||
moved to `actix_web::http` module
|
||||
|
||||
* `actix_web::header` moved to `actix_web::http::header`
|
||||
|
||||
* `NormalizePath` moved to `actix_web::http` module
|
||||
|
||||
* `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
|
||||
shortcut for `actix_web::server::HttpServer::new()`
|
||||
|
||||
* `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
|
||||
|
||||
* `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead.
|
||||
|
||||
* `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
|
||||
|
||||
* `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
|
||||
functions should be used instead
|
||||
|
||||
* `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
|
||||
instead of `Result<_, http::Error>`
|
||||
|
||||
* `Application` renamed to a `App`
|
||||
|
||||
* `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev`
|
79
README.md
Normal file
79
README.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Actix web [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-web) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
Actix web is a simple, pragmatic and extremely fast web framework for Rust.
|
||||
|
||||
* Supported *HTTP/1.x* and *HTTP/2.0* protocols
|
||||
* Streaming and pipelining
|
||||
* Keep-alive and slow requests handling
|
||||
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
|
||||
* Transparent content compression/decompression (br, gzip, deflate)
|
||||
* Configurable [request routing](https://actix.rs/docs/url-dispatch/)
|
||||
* Multipart streams
|
||||
* Static assets
|
||||
* SSL support with 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)
|
||||
* Supports [Actix actor framework](https://github.com/actix/actix)
|
||||
|
||||
## Documentation & community resources
|
||||
|
||||
* [User Guide](https://actix.rs/docs/)
|
||||
* [API Documentation (1.0)](https://docs.rs/actix-web/)
|
||||
* [API Documentation (0.7)](https://docs.rs/actix-web/0.7.19/actix_web/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-web](https://crates.io/crates/actix-web)
|
||||
* Minimum supported Rust version: 1.36 or later
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
use actix_web::{web, App, HttpServer, Responder};
|
||||
|
||||
fn index(info: web::Path<(u32, String)>) -> impl Responder {
|
||||
format!("Hello {}! id:{}", info.1, info.0)
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(
|
||||
|| App::new().service(
|
||||
web::resource("/{id}/{name}/index.html").to(index)))
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
}
|
||||
```
|
||||
|
||||
### 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/)
|
||||
* [SSL / HTTP/2.0](https://github.com/actix/examples/tree/master/tls/)
|
||||
* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/)
|
||||
* [Json](https://github.com/actix/examples/tree/master/json/)
|
||||
|
||||
You may consider checking out
|
||||
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r18)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||
|
||||
at your option.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Contribution to the actix-web crate is organized under the terms of the
|
||||
Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to
|
||||
intervene to uphold that code of conduct.
|
9
actix-cors/CHANGES.md
Normal file
9
actix-cors/CHANGES.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Changes
|
||||
|
||||
## [0.1.1] - unreleased
|
||||
|
||||
* Bump `derive_more` crate version to 0.15.0
|
||||
|
||||
## [0.1.0] - 2019-06-15
|
||||
|
||||
* Move cors middleware to separate crate
|
23
actix-cors/Cargo.toml
Normal file
23
actix-cors/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "actix-cors"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Cross-origin resource sharing (CORS) for Actix applications."
|
||||
readme = "README.md"
|
||||
keywords = ["web", "framework"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
documentation = "https://docs.rs/actix-cors/"
|
||||
license = "MIT/Apache-2.0"
|
||||
edition = "2018"
|
||||
#workspace = ".."
|
||||
|
||||
[lib]
|
||||
name = "actix_cors"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "1.0.0"
|
||||
actix-service = "0.4.0"
|
||||
derive_more = "0.15.0"
|
||||
futures = "0.1.25"
|
9
actix-cors/README.md
Normal file
9
actix-cors/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# 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)
|
||||
|
||||
## 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
|
1175
actix-cors/src/lib.rs
Normal file
1175
actix-cors/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,211 +1,64 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
## [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.6.0 - 2022-02-25
|
||||
- No significant changes since `0.6.0-beta.16`.
|
||||
## [0.1.6] - 2019-10-14
|
||||
|
||||
* Add option to redirect to a slash-ended path `Files` #1132
|
||||
|
||||
## 0.6.0-beta.16 - 2022-01-31
|
||||
- No significant changes since `0.6.0-beta.15`.
|
||||
## [0.1.5] - 2019-10-08
|
||||
|
||||
* Bump up `mime_guess` crate version to 2.0.1
|
||||
|
||||
## 0.6.0-beta.15 - 2022-01-21
|
||||
- No significant changes since `0.6.0-beta.14`.
|
||||
* Bump up `percent-encoding` crate version to 2.1
|
||||
|
||||
* Allow user defined request guards for `Files` #1113
|
||||
|
||||
## 0.6.0-beta.14 - 2022-01-14
|
||||
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
|
||||
## [0.1.4] - 2019-07-20
|
||||
|
||||
[#2583]: https://github.com/actix/actix-web/pull/2583
|
||||
* Allow to disable `Content-Disposition` header #686
|
||||
|
||||
## [0.1.3] - 2019-06-28
|
||||
|
||||
## 0.6.0-beta.13 - 2022-01-04
|
||||
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
|
||||
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
* Do not set `Content-Length` header, let actix-http set it #930
|
||||
|
||||
[#2398]: https://github.com/actix/actix-web/pull/2398
|
||||
## [0.1.2] - 2019-06-13
|
||||
|
||||
* Content-Length is 0 for NamedFile HEAD request #914
|
||||
|
||||
## 0.6.0-beta.12 - 2021-12-29
|
||||
- No significant changes since `0.6.0-beta.11`.
|
||||
* Fix ring dependency from actix-web default features for #741
|
||||
|
||||
## [0.1.1] - 2019-06-01
|
||||
|
||||
## 0.6.0-beta.11 - 2021-12-27
|
||||
- No significant changes since `0.6.0-beta.10`.
|
||||
* Static files are incorrectly served as both chunked and with length #812
|
||||
|
||||
## [0.1.0] - 2019-05-25
|
||||
|
||||
## 0.6.0-beta.10 - 2021-12-11
|
||||
- No significant changes since `0.6.0-beta.9`.
|
||||
* NamedFile last-modified check always fails due to nano-seconds
|
||||
in file modified date #820
|
||||
|
||||
## [0.1.0-beta.4] - 2019-05-12
|
||||
|
||||
## 0.6.0-beta.9 - 2021-11-22
|
||||
- Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
|
||||
- Add `NamedFile::open_async`. [#2408]
|
||||
- Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
|
||||
- The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408]
|
||||
- The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408]
|
||||
- Add `impl Clone` for `FilesService`. [#2408]
|
||||
* Update actix-web to beta.4
|
||||
|
||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||
[#2453]: https://github.com/actix/actix-web/pull/2453
|
||||
## [0.1.0-beta.1] - 2019-04-20
|
||||
|
||||
* Update actix-web to beta.1
|
||||
|
||||
## 0.6.0-beta.8 - 2021-10-20
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
## [0.1.0-alpha.6] - 2019-04-14
|
||||
|
||||
* Update actix-web to alpha6
|
||||
|
||||
## 0.6.0-beta.7 - 2021-09-09
|
||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||
## [0.1.0-alpha.4] - 2019-04-08
|
||||
|
||||
* Update actix-web to alpha4
|
||||
|
||||
## 0.6.0-beta.6 - 2021-06-26
|
||||
- Added `Files::path_filter()`. [#2274]
|
||||
- `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
|
||||
## [0.1.0-alpha.2] - 2019-04-02
|
||||
|
||||
[#2274]: https://github.com/actix/actix-web/pull/2274
|
||||
[#2228]: https://github.com/actix/actix-web/pull/2228
|
||||
* Add default handler support
|
||||
|
||||
## [0.1.0-alpha.1] - 2019-03-28
|
||||
|
||||
## 0.6.0-beta.5 - 2021-06-17
|
||||
- `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
|
||||
- For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
|
||||
- `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
|
||||
- `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
|
||||
|
||||
[#2135]: https://github.com/actix/actix-web/pull/2135
|
||||
[#2156]: https://github.com/actix/actix-web/pull/2156
|
||||
[#2225]: https://github.com/actix/actix-web/pull/2225
|
||||
[#2257]: https://github.com/actix/actix-web/pull/2257
|
||||
|
||||
|
||||
## 0.6.0-beta.4 - 2021-04-02
|
||||
- Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
|
||||
|
||||
[#2046]: https://github.com/actix/actix-web/pull/2046
|
||||
|
||||
|
||||
## 0.6.0-beta.3 - 2021-03-09
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 0.6.0-beta.2 - 2021-02-10
|
||||
- Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
|
||||
- Replace `v_htmlescape` with `askama_escape`. [#1953]
|
||||
|
||||
[#1887]: https://github.com/actix/actix-web/pull/1887
|
||||
[#1953]: https://github.com/actix/actix-web/pull/1953
|
||||
|
||||
|
||||
## 0.6.0-beta.1 - 2021-01-07
|
||||
- `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]
|
||||
|
||||
[#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
|
||||
- Fix BodyEncoding trait import #1220
|
||||
|
||||
|
||||
## 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
|
||||
- Add option to redirect to a slash-ended path `Files` #1132
|
||||
|
||||
|
||||
## 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
|
||||
- Allow to disable `Content-Disposition` header #686
|
||||
|
||||
|
||||
## 0.1.3 - 2019-06-28
|
||||
- Do not set `Content-Length` header, let actix-http set it #930
|
||||
|
||||
|
||||
## 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
|
||||
- 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-beta.4 - 2019-05-12
|
||||
- Update actix-web to beta.4
|
||||
|
||||
|
||||
## 0.1.0-beta.1 - 2019-04-20
|
||||
- Update actix-web to beta.1
|
||||
|
||||
|
||||
## 0.1.0-alpha.6 - 2019-04-14
|
||||
- Update actix-web to alpha6
|
||||
|
||||
|
||||
## 0.1.0-alpha.4 - 2019-04-08
|
||||
- Update actix-web to alpha4
|
||||
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-04-02
|
||||
- Add default handler support
|
||||
|
||||
|
||||
## 0.1.0-alpha.1 - 2019-03-28
|
||||
- Initial impl
|
||||
* Initial impl
|
||||
|
@ -1,48 +1,35 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
]
|
||||
description = "Static file serving for Actix Web"
|
||||
version = "0.1.7"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Static files support for actix web."
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
documentation = "https://docs.rs/actix-files/"
|
||||
categories = ["asynchronous", "web-programming::http-server"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
license = "MIT/Apache-2.0"
|
||||
edition = "2018"
|
||||
workspace = ".."
|
||||
|
||||
[lib]
|
||||
name = "actix_files"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3"
|
||||
actix-service = "2"
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4", default-features = false }
|
||||
|
||||
askama_escape = "0.10"
|
||||
actix-web = { version = "1.0.8", default-features = false }
|
||||
actix-http = "0.2.11"
|
||||
actix-service = "0.4.1"
|
||||
bitflags = "1"
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
http-range = "0.1.4"
|
||||
bytes = "0.4"
|
||||
futures = "0.1.25"
|
||||
derive_more = "0.15.0"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2.7"
|
||||
|
||||
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
|
||||
v_htmlescape = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.13"
|
||||
actix-web = "4.0.0"
|
||||
tempfile = "3.2"
|
||||
actix-web = { version = "1.0.8", features=["ssl"] }
|
||||
|
@ -1,18 +1,9 @@
|
||||
# actix-files
|
||||
# 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)
|
||||
|
||||
> Static file serving for Actix Web
|
||||
## Documentation & community resources
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.0)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.0)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-files)
|
||||
- [Example Project](https://github.com/actix/examples/tree/master/basics/static-files)
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
* [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.33 or later
|
||||
|
@ -1,218 +0,0 @@
|
||||
use std::{
|
||||
cmp, fmt,
|
||||
future::Future,
|
||||
io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_web::{error::Error, web::Bytes};
|
||||
use futures_core::{ready, Stream};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
use bytes::BytesMut;
|
||||
|
||||
use super::named::File;
|
||||
|
||||
pin_project! {
|
||||
/// Adapter to read a `std::file::File` in chunks.
|
||||
#[doc(hidden)]
|
||||
pub struct ChunkedReadFile<F, Fut> {
|
||||
size: u64,
|
||||
offset: u64,
|
||||
#[pin]
|
||||
state: ChunkedReadFileState<Fut>,
|
||||
counter: u64,
|
||||
callback: F,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pin_project! {
|
||||
#[project = ChunkedReadFileStateProj]
|
||||
#[project_replace = ChunkedReadFileStateProjReplace]
|
||||
enum ChunkedReadFileState<Fut> {
|
||||
File { file: Option<File>, },
|
||||
Future { #[pin] fut: Fut },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
pin_project! {
|
||||
#[project = ChunkedReadFileStateProj]
|
||||
#[project_replace = ChunkedReadFileStateProjReplace]
|
||||
enum ChunkedReadFileState<Fut> {
|
||||
File { file: Option<(File, BytesMut)> },
|
||||
Future { #[pin] fut: Fut },
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, Fut> fmt::Debug for ChunkedReadFile<F, Fut> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("ChunkedReadFile")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_chunked_read(
|
||||
size: u64,
|
||||
offset: u64,
|
||||
file: File,
|
||||
) -> impl Stream<Item = Result<Bytes, Error>> {
|
||||
ChunkedReadFile {
|
||||
size,
|
||||
offset,
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
state: ChunkedReadFileState::File { file: Some(file) },
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
state: ChunkedReadFileState::File {
|
||||
file: Some((file, BytesMut::new())),
|
||||
},
|
||||
counter: 0,
|
||||
callback: chunked_read_file_callback,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
async fn chunked_read_file_callback(
|
||||
mut file: File,
|
||||
offset: u64,
|
||||
max_bytes: usize,
|
||||
) -> Result<(File, Bytes), Error> {
|
||||
use io::{Read as _, Seek as _};
|
||||
|
||||
let res = actix_web::web::block(move || {
|
||||
let mut buf = Vec::with_capacity(max_bytes);
|
||||
|
||||
file.seek(io::SeekFrom::Start(offset))?;
|
||||
|
||||
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
||||
|
||||
if n_bytes == 0 {
|
||||
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
|
||||
} else {
|
||||
Ok((file, Bytes::from(buf)))
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
async fn chunked_read_file_callback(
|
||||
file: File,
|
||||
offset: u64,
|
||||
max_bytes: usize,
|
||||
mut bytes_mut: BytesMut,
|
||||
) -> io::Result<(File, Bytes, BytesMut)> {
|
||||
bytes_mut.reserve(max_bytes);
|
||||
|
||||
let (res, mut bytes_mut) = file.read_at(bytes_mut, offset).await;
|
||||
let n_bytes = res?;
|
||||
|
||||
if n_bytes == 0 {
|
||||
return Err(io::ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
|
||||
let bytes = bytes_mut.split_to(n_bytes).freeze();
|
||||
|
||||
Ok((file, bytes, bytes_mut))
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||
where
|
||||
F: Fn(File, u64, usize, BytesMut) -> Fut,
|
||||
Fut: Future<Output = io::Result<(File, Bytes, BytesMut)>>,
|
||||
{
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut().project();
|
||||
match this.state.as_mut().project() {
|
||||
ChunkedReadFileStateProj::File { file } => {
|
||||
let size = *this.size;
|
||||
let offset = *this.offset;
|
||||
let counter = *this.counter;
|
||||
|
||||
if size == counter {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
|
||||
let (file, bytes_mut) = file
|
||||
.take()
|
||||
.expect("ChunkedReadFile polled after completion");
|
||||
|
||||
let fut = (this.callback)(file, offset, max_bytes, bytes_mut);
|
||||
|
||||
this.state
|
||||
.project_replace(ChunkedReadFileState::Future { fut });
|
||||
|
||||
self.poll_next(cx)
|
||||
}
|
||||
}
|
||||
ChunkedReadFileStateProj::Future { fut } => {
|
||||
let (file, bytes, bytes_mut) = ready!(fut.poll(cx))?;
|
||||
|
||||
this.state.project_replace(ChunkedReadFileState::File {
|
||||
file: Some((file, bytes_mut)),
|
||||
});
|
||||
|
||||
*this.offset += bytes.len() as u64;
|
||||
*this.counter += bytes.len() as u64;
|
||||
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||
where
|
||||
F: Fn(File, u64, usize) -> Fut,
|
||||
Fut: Future<Output = Result<(File, Bytes), Error>>,
|
||||
{
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut().project();
|
||||
match this.state.as_mut().project() {
|
||||
ChunkedReadFileStateProj::File { file } => {
|
||||
let size = *this.size;
|
||||
let offset = *this.offset;
|
||||
let counter = *this.counter;
|
||||
|
||||
if size == counter {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
|
||||
let file = file
|
||||
.take()
|
||||
.expect("ChunkedReadFile polled after completion");
|
||||
|
||||
let fut = (this.callback)(file, offset, max_bytes);
|
||||
|
||||
this.state
|
||||
.project_replace(ChunkedReadFileState::Future { fut });
|
||||
|
||||
self.poll_next(cx)
|
||||
}
|
||||
}
|
||||
ChunkedReadFileStateProj::Future { fut } => {
|
||||
let (file, bytes) = ready!(fut.poll(cx))?;
|
||||
|
||||
this.state
|
||||
.project_replace(ChunkedReadFileState::File { file: Some(file) });
|
||||
|
||||
*this.offset += bytes.len() as u64;
|
||||
*this.counter += bytes.len() as u64;
|
||||
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
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>;
|
||||
|
||||
/// Returns percent encoded file URL path.
|
||||
macro_rules! encode_file_url {
|
||||
($path:ident) => {
|
||||
utf8_percent_encode(&$path, CONTROLS)
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns HTML entity encoded formatter.
|
||||
///
|
||||
/// ```plain
|
||||
/// " => "
|
||||
/// & => &
|
||||
/// ' => '
|
||||
/// < => <
|
||||
/// > => >
|
||||
/// / => /
|
||||
/// ```
|
||||
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),
|
||||
))
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
use actix_web::{http::StatusCode, ResponseError};
|
||||
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
||||
use derive_more::Display;
|
||||
|
||||
/// Errors which can occur when serving static files.
|
||||
#[derive(Display, Debug, PartialEq)]
|
||||
pub enum FilesError {
|
||||
/// Path is not a directory
|
||||
#[allow(dead_code)]
|
||||
#[display(fmt = "Path is not a directory. Unable to serve static files")]
|
||||
IsNotDirectory,
|
||||
|
||||
@ -16,35 +15,27 @@ pub enum FilesError {
|
||||
|
||||
/// Return `NotFound` for `FilesError`
|
||||
impl ResponseError for FilesError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::NOT_FOUND
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Display, Debug, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum UriSegmentError {
|
||||
/// The segment started with the wrapped invalid character.
|
||||
#[display(fmt = "The segment started with the wrapped invalid character")]
|
||||
BadStart(char),
|
||||
|
||||
/// The segment contained the wrapped invalid character.
|
||||
#[display(fmt = "The segment contained the wrapped invalid character")]
|
||||
BadChar(char),
|
||||
|
||||
/// The segment ended with the wrapped invalid character.
|
||||
#[display(fmt = "The segment ended with the wrapped invalid character")]
|
||||
BadEnd(char),
|
||||
|
||||
/// The path is not a valid UTF-8 string after doing percent decoding.
|
||||
#[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")]
|
||||
NotValidUtf8,
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UriSegmentError`
|
||||
impl ResponseError for UriSegmentError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
|
@ -1,392 +0,0 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fmt, io,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
||||
use actix_web::{
|
||||
dev::{
|
||||
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
|
||||
ServiceResponse,
|
||||
},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::header::DispositionType,
|
||||
HttpRequest,
|
||||
};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
|
||||
use crate::{
|
||||
directory_listing, named,
|
||||
service::{FilesService, FilesServiceInner},
|
||||
Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
|
||||
};
|
||||
|
||||
/// Static files handling service.
|
||||
///
|
||||
/// `Files` service must be registered with `App::service()` method.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::App;
|
||||
/// use actix_files::Files;
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .service(Files::new("/static", "."));
|
||||
/// ```
|
||||
pub struct Files {
|
||||
mount_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>>,
|
||||
path_filter: Option<Rc<PathFilter>>,
|
||||
file_flags: named::Flags,
|
||||
use_guards: Option<Rc<dyn Guard>>,
|
||||
guards: Vec<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,
|
||||
mount_path: self.mount_path.clone(),
|
||||
mime_override: self.mime_override.clone(),
|
||||
path_filter: self.path_filter.clone(),
|
||||
use_guards: self.use_guards.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` utilizes the existing Tokio thread-pool for blocking filesystem operations.
|
||||
/// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
|
||||
/// the number of server [workers](actix_web::HttpServer::workers), by default.
|
||||
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 {
|
||||
mount_path: mount_path.trim_end_matches('/').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,
|
||||
path_filter: None,
|
||||
file_flags: named::Flags::default(),
|
||||
use_guards: None,
|
||||
guards: Vec::new(),
|
||||
hidden_files: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show files listing for directories.
|
||||
///
|
||||
/// By default show files listing is disabled.
|
||||
///
|
||||
/// When used with [`Files::index_file()`], files listing is shown as a fallback
|
||||
/// when the index file is not found.
|
||||
pub fn show_files_listing(mut self) -> Self {
|
||||
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
|
||||
}
|
||||
|
||||
/// Sets path filtering closure.
|
||||
///
|
||||
/// The path provided to the closure is relative to `serve_from` path.
|
||||
/// You can safely join this path with the `serve_from` path to get the real path.
|
||||
/// However, the real path may not exist since the filter is called before checking path existence.
|
||||
///
|
||||
/// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise,
|
||||
/// `404 Not Found` is returned.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use actix_files::Files;
|
||||
///
|
||||
/// // prevent searching subdirectories and following symlinks
|
||||
/// let files_service = Files::new("/", "./static").path_filter(|path, _| {
|
||||
/// path.components().count() == 1
|
||||
/// && Path::new("./static")
|
||||
/// .join(path)
|
||||
/// .symlink_metadata()
|
||||
/// .map(|m| !m.file_type().is_symlink())
|
||||
/// .unwrap_or(false)
|
||||
/// });
|
||||
/// ```
|
||||
pub fn path_filter<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: Fn(&Path, &RequestHead) -> bool + 'static,
|
||||
{
|
||||
self.path_filter = Some(Rc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index file
|
||||
///
|
||||
/// Shows specific index file for directories instead of
|
||||
/// showing files listing.
|
||||
///
|
||||
/// If the index file is not found, files listing is shown as a fallback if
|
||||
/// [`Files::show_files_listing()`] is set.
|
||||
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
|
||||
self.index = Some(index.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use ETag or not.
|
||||
///
|
||||
/// Default is true.
|
||||
pub fn use_etag(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::ETAG, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use Last-Modified or not.
|
||||
///
|
||||
/// Default is true.
|
||||
pub fn use_last_modified(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::LAST_MD, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether text responses should signal a UTF-8 encoding.
|
||||
///
|
||||
/// Default is false (but will default to true in a future version).
|
||||
pub fn prefer_utf8(mut self, value: bool) -> Self {
|
||||
self.file_flags.set(named::Flags::PREFER_UTF8, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a routing guard.
|
||||
///
|
||||
/// Use this to allow multiple chained file services that respond to strictly different
|
||||
/// properties of a request. Due to the way routing works, if a guard check returns true and the
|
||||
/// request starts being handled by the file service, it will not be able to back-out and try
|
||||
/// the next service, you will simply get a 404 (or 405) error response.
|
||||
///
|
||||
/// To allow `POST` requests to retrieve files, see [`Files::use_guards`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{guard::Header, App};
|
||||
/// use actix_files::Files;
|
||||
///
|
||||
/// App::new().service(
|
||||
/// Files::new("/","/my/site/files")
|
||||
/// .guard(Header("Host", "example.com"))
|
||||
/// );
|
||||
/// ```
|
||||
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
|
||||
self.guards.push(Rc::new(guard));
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies guard to check before fetching directory listings or files.
|
||||
///
|
||||
/// Note that this guard has no effect on routing; it's main use is to guard on the request's
|
||||
/// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
|
||||
/// See [`Files::guard`] for routing guards.
|
||||
pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
|
||||
self.use_guards = Some(Rc::new(guard));
|
||||
self
|
||||
}
|
||||
|
||||
/// See [`Files::method_guard`].
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
|
||||
pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
|
||||
self.method_guard(guard)
|
||||
}
|
||||
|
||||
/// Disable `Content-Disposition` header.
|
||||
///
|
||||
/// By default Content-Disposition` header is enabled.
|
||||
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.
|
||||
///
|
||||
/// # Examples
|
||||
/// Setting a fallback static file handler:
|
||||
/// ```
|
||||
/// use actix_files::{Files, NamedFile};
|
||||
/// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
|
||||
///
|
||||
/// # fn run() -> Result<(), actix_web::Error> {
|
||||
/// let files = Files::new("/", "./static")
|
||||
/// .index_file("index.html")
|
||||
/// .default_handler(fn_service(|req: ServiceRequest| async {
|
||||
/// let (req, _) = req.into_parts();
|
||||
/// let file = NamedFile::open_async("./static/404.html").await?;
|
||||
/// let res = file.into_response(&req);
|
||||
/// Ok(ServiceResponse::new(req, res))
|
||||
/// }));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn default_handler<F, U>(mut self, f: F) -> Self
|
||||
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.
|
||||
pub fn use_hidden_files(mut self) -> Self {
|
||||
self.hidden_files = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpServiceFactory for Files {
|
||||
fn register(mut self, config: &mut AppService) {
|
||||
let guards = if self.guards.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let guards = std::mem::take(&mut self.guards);
|
||||
Some(
|
||||
guards
|
||||
.into_iter()
|
||||
.map(|guard| -> Box<dyn Guard> { Box::new(guard) })
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
|
||||
if self.default.borrow().is_none() {
|
||||
*self.default.borrow_mut() = Some(config.default_service());
|
||||
}
|
||||
|
||||
let rdef = if config.is_root() {
|
||||
ResourceDef::root_prefix(&self.mount_path)
|
||||
} else {
|
||||
ResourceDef::prefix(&self.mount_path)
|
||||
};
|
||||
|
||||
config.register_service(rdef, guards, 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 inner = FilesServiceInner {
|
||||
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(),
|
||||
path_filter: self.path_filter.clone(),
|
||||
file_flags: self.file_flags,
|
||||
guards: self.use_guards.clone(),
|
||||
hidden_files: self.hidden_files,
|
||||
};
|
||||
|
||||
if let Some(ref default) = *self.default.borrow() {
|
||||
let fut = default.new_service(());
|
||||
Box::pin(async {
|
||||
match fut.await {
|
||||
Ok(default) => {
|
||||
inner.default = Some(default);
|
||||
Ok(FilesService(Rc::new(inner)))
|
||||
}
|
||||
Err(_) => Err(()),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,78 +1,46 @@
|
||||
use std::{
|
||||
fs::Metadata,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::fs::{File, Metadata};
|
||||
use std::io;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use actix_web::{
|
||||
body::{self, BoxBody, SizedStream},
|
||||
dev::{
|
||||
self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
|
||||
ServiceRequest, ServiceResponse,
|
||||
},
|
||||
http::{
|
||||
header::{
|
||||
self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
|
||||
DispositionType, ExtendedValue, HeaderValue,
|
||||
},
|
||||
StatusCode,
|
||||
},
|
||||
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||
};
|
||||
use bitflags::bitflags;
|
||||
use bytes::Bytes;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use futures_core::{future::LocalBoxFuture, Stream};
|
||||
use mime;
|
||||
use mime_guess::from_path;
|
||||
|
||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||
use actix_http::body::SizedStream;
|
||||
use actix_web::http::header::{
|
||||
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
|
||||
};
|
||||
use actix_web::http::{ContentEncoding, StatusCode};
|
||||
use actix_web::middleware::BodyEncoding;
|
||||
use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use crate::range::HttpRange;
|
||||
use crate::ChunkedReadFile;
|
||||
|
||||
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::from_bits_truncate(0b0000_1111)
|
||||
Flags::all()
|
||||
}
|
||||
}
|
||||
|
||||
/// A file with an associated name.
|
||||
///
|
||||
/// `NamedFile` can be registered as services:
|
||||
/// ```
|
||||
/// use actix_web::App;
|
||||
/// use actix_files::NamedFile;
|
||||
///
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let file = NamedFile::open_async("./static/index.html").await?;
|
||||
/// let app = App::new().service(file);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// They can also be returned from handlers:
|
||||
/// ```
|
||||
/// use actix_web::{Responder, get};
|
||||
/// use actix_files::NamedFile;
|
||||
///
|
||||
/// #[get("/")]
|
||||
/// async fn index() -> impl Responder {
|
||||
/// NamedFile::open_async("./static/index.html").await
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Deref, DerefMut)]
|
||||
#[derive(Debug)]
|
||||
pub struct NamedFile {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
file: File,
|
||||
path: PathBuf,
|
||||
file: File,
|
||||
modified: Option<SystemTime>,
|
||||
pub(crate) md: Metadata,
|
||||
pub(crate) flags: Flags,
|
||||
@ -82,13 +50,6 @@ pub struct NamedFile {
|
||||
pub(crate) encoding: Option<ContentEncoding>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pub(crate) use std::fs::File;
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
pub(crate) use tokio_uring::fs::File;
|
||||
|
||||
use super::chunked;
|
||||
|
||||
impl NamedFile {
|
||||
/// Creates an instance from a previously opened file.
|
||||
///
|
||||
@ -96,19 +57,20 @@ impl NamedFile {
|
||||
/// `ContentDisposition` headers.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// use std::{
|
||||
/// io::{self, Write as _},
|
||||
/// env,
|
||||
/// fs::File
|
||||
/// };
|
||||
/// use actix_files::NamedFile;
|
||||
///
|
||||
/// let mut file = File::create("foo.txt")?;
|
||||
/// file.write_all(b"Hello, world!")?;
|
||||
/// let named_file = NamedFile::from_file(file, "bar.txt")?;
|
||||
/// # std::fs::remove_file("foo.txt");
|
||||
/// Ok(())
|
||||
/// ```rust
|
||||
/// use actix_files::NamedFile;
|
||||
/// use std::io::{self, Write};
|
||||
/// use std::env;
|
||||
/// use std::fs::File;
|
||||
///
|
||||
/// fn main() -> io::Result<()> {
|
||||
/// let mut file = File::create("foo.txt")?;
|
||||
/// file.write_all(b"Hello, world!")?;
|
||||
/// let named_file = NamedFile::from_file(file, "bar.txt")?;
|
||||
/// # std::fs::remove_file("foo.txt");
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn from_file<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
@ -127,20 +89,12 @@ impl NamedFile {
|
||||
};
|
||||
|
||||
let ct = from_path(&path).first_or_octet_stream();
|
||||
|
||||
let disposition = match ct.type_() {
|
||||
let disposition_type = match ct.type_() {
|
||||
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
||||
mime::APPLICATION => match ct.subtype() {
|
||||
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
||||
name if name == "wasm" => DispositionType::Inline,
|
||||
_ => DispositionType::Attachment,
|
||||
},
|
||||
_ => DispositionType::Attachment,
|
||||
};
|
||||
|
||||
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")),
|
||||
@ -148,42 +102,16 @@ impl NamedFile {
|
||||
value: filename.into_owned().into_bytes(),
|
||||
}))
|
||||
}
|
||||
|
||||
let cd = ContentDisposition {
|
||||
disposition,
|
||||
parameters,
|
||||
disposition: disposition_type,
|
||||
parameters: parameters,
|
||||
};
|
||||
|
||||
(ct, cd)
|
||||
};
|
||||
|
||||
let md = {
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
{
|
||||
file.metadata()?
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
{
|
||||
use std::os::unix::prelude::{AsRawFd, FromRawFd};
|
||||
|
||||
let fd = file.as_raw_fd();
|
||||
|
||||
// SAFETY: fd is borrowed and lives longer than the unsafe block
|
||||
unsafe {
|
||||
let file = std::fs::File::from_raw_fd(fd);
|
||||
let md = file.metadata();
|
||||
// SAFETY: forget the fd before exiting block in success or error case but don't
|
||||
// run destructor (that would close file handle)
|
||||
std::mem::forget(file);
|
||||
md?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let md = file.metadata()?;
|
||||
let modified = md.modified().ok();
|
||||
let encoding = None;
|
||||
|
||||
Ok(NamedFile {
|
||||
path,
|
||||
file,
|
||||
@ -200,43 +128,14 @@ impl NamedFile {
|
||||
/// Attempts to open a file in read-only mode.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_files::NamedFile;
|
||||
///
|
||||
/// let file = NamedFile::open("foo.txt");
|
||||
/// ```
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||
let file = File::open(&path)?;
|
||||
Self::from_file(file, path)
|
||||
}
|
||||
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
/// Attempts to open a file asynchronously in read-only mode.
|
||||
///
|
||||
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
|
||||
/// Otherwise, it will be just like [`open`][Self::open].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_files::NamedFile;
|
||||
/// # async fn open() {
|
||||
/// let file = NamedFile::open_async("foo.txt").await.unwrap();
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn open_async<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||
let file = {
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
{
|
||||
File::open(&path)?
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
{
|
||||
File::open(&path).await?
|
||||
}
|
||||
};
|
||||
|
||||
Self::from_file(file, path)
|
||||
Self::from_file(File::open(&path)?, path)
|
||||
}
|
||||
|
||||
/// Returns reference to the underlying `File` object.
|
||||
@ -248,12 +147,13 @@ impl NamedFile {
|
||||
/// Retrieve the path of this file.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::io;
|
||||
/// use actix_files::NamedFile;
|
||||
///
|
||||
/// # async fn path() -> io::Result<()> {
|
||||
/// let file = NamedFile::open_async("test.txt").await?;
|
||||
/// # fn path() -> io::Result<()> {
|
||||
/// let file = NamedFile::open("test.txt")?;
|
||||
/// assert_eq!(file.path().as_os_str(), "foo.txt");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@ -269,21 +169,21 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred
|
||||
/// from the filename extension.
|
||||
/// Set the MIME Content-Type for serving this file. By default
|
||||
/// the Content-Type is inferred from the filename extension.
|
||||
#[inline]
|
||||
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
||||
self.content_type = mime_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Content-Disposition for serving this file. This allows changing the
|
||||
/// `inline/attachment` disposition as well as the filename sent to the peer.
|
||||
///
|
||||
/// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
|
||||
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, and the
|
||||
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
||||
/// (using `to_string_lossy`).
|
||||
/// Set the Content-Disposition for serving this file. This allows
|
||||
/// changing the inline/attachment disposition as well as the filename
|
||||
/// sent to the peer. By default the disposition is `inline` for text,
|
||||
/// 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).
|
||||
#[inline]
|
||||
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
||||
self.content_disposition = cd;
|
||||
@ -300,56 +200,39 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets content encoding for this file.
|
||||
///
|
||||
/// This prevents the `Compress` middleware from modifying the file contents and signals to
|
||||
/// browsers/clients how to decode it. For example, if serving a compressed HTML file (e.g.,
|
||||
/// `index.html.gz`) then use `.set_content_encoding(ContentEncoding::Gzip)`.
|
||||
/// Set content encoding for serving this file
|
||||
#[inline]
|
||||
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
||||
self.encoding = Some(enc);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to return `ETag` header in response.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
///Specifies whether to use ETag or not.
|
||||
///
|
||||
///Default is true.
|
||||
pub fn use_etag(mut self, value: bool) -> Self {
|
||||
self.flags.set(Flags::ETAG, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to return `Last-Modified` header in response.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
///Specifies whether to use Last-Modified or not.
|
||||
///
|
||||
///Default is true.
|
||||
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
|
||||
}
|
||||
|
||||
/// Creates an `ETag` in a format is similar to Apache's.
|
||||
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
||||
// This etag format is similar to Apache's.
|
||||
self.modified.as_ref().map(|mtime| {
|
||||
let ino = {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt as _;
|
||||
|
||||
self.md.ino()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
0
|
||||
@ -359,8 +242,7 @@ impl NamedFile {
|
||||
let dur = mtime
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("modification time must be after epoch");
|
||||
|
||||
header::EntityTag::new_strong(format!(
|
||||
header::EntityTag::strong(format!(
|
||||
"{:x}:{:x}:{:x}:{:x}",
|
||||
ino,
|
||||
self.md.len(),
|
||||
@ -373,34 +255,82 @@ impl NamedFile {
|
||||
pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
|
||||
self.modified.map(|mtime| mtime.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an `HttpResponse` with file as a streaming body.
|
||||
pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> {
|
||||
impl Deref for NamedFile {
|
||||
type Target = File;
|
||||
|
||||
fn deref(&self) -> &File {
|
||||
&self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NamedFile {
|
||||
fn deref_mut(&mut self) -> &mut File {
|
||||
&mut self.file
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
|
||||
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 {
|
||||
if item.strong_eq(some_etag) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
|
||||
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 {
|
||||
if item.weak_eq(some_etag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for NamedFile {
|
||||
type Error = Error;
|
||||
type Future = Result<HttpResponse, Error>;
|
||||
|
||||
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
||||
if self.status_code != StatusCode::OK {
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
let ct = if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
equiv_utf8_text(self.content_type.clone())
|
||||
} else {
|
||||
self.content_type
|
||||
};
|
||||
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
res.insert_header((
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
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 {
|
||||
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||
resp.encoding(current_encoding);
|
||||
}
|
||||
|
||||
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
|
||||
|
||||
return res.streaming(reader);
|
||||
let reader = ChunkedReadFile {
|
||||
size: self.md.len(),
|
||||
offset: 0,
|
||||
file: Some(self.file),
|
||||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
return Ok(resp.streaming(reader));
|
||||
}
|
||||
|
||||
let etag = if self.flags.contains(Flags::ETAG) {
|
||||
@ -408,7 +338,6 @@ impl NamedFile {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let last_modified = if self.flags.contains(Flags::LAST_MD) {
|
||||
self.last_modified()
|
||||
} else {
|
||||
@ -421,11 +350,10 @@ impl NamedFile {
|
||||
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
|
||||
(last_modified, req.get_header())
|
||||
{
|
||||
let t1: SystemTime = (*m).into();
|
||||
let t2: SystemTime = (*since).into();
|
||||
|
||||
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.as_secs() > t2.as_secs(),
|
||||
(Ok(t1), Ok(t2)) => t1 > t2,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
@ -435,203 +363,87 @@ 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).into();
|
||||
let t2: SystemTime = (*since).into();
|
||||
|
||||
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.as_secs() <= t2.as_secs(),
|
||||
(Ok(t1), Ok(t2)) => t1 <= t2,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
let ct = if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
equiv_utf8_text(self.content_type.clone())
|
||||
} else {
|
||||
self.content_type
|
||||
};
|
||||
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
res.insert_header((
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
});
|
||||
// default compressing
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||
resp.encoding(current_encoding);
|
||||
}
|
||||
|
||||
if let Some(lm) = last_modified {
|
||||
res.insert_header((header::LAST_MODIFIED, lm.to_string()));
|
||||
}
|
||||
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(etag) = etag {
|
||||
res.insert_header((header::ETAG, etag.to_string()));
|
||||
}
|
||||
|
||||
res.insert_header((header::ACCEPT_RANGES, "bytes"));
|
||||
resp.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(ranges_header) = ranges.to_str() {
|
||||
if let Ok(ranges) = HttpRange::parse(ranges_header, length) {
|
||||
length = ranges[0].length;
|
||||
offset = ranges[0].start;
|
||||
|
||||
// don't allow compression middleware to modify partial content
|
||||
res.insert_header((
|
||||
header::CONTENT_ENCODING,
|
||||
HeaderValue::from_static("identity"),
|
||||
));
|
||||
|
||||
res.insert_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;
|
||||
resp.encoding(ContentEncoding::Identity);
|
||||
resp.header(
|
||||
header::CONTENT_RANGE,
|
||||
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
|
||||
));
|
||||
format!(
|
||||
"bytes {}-{}/{}",
|
||||
offset,
|
||||
offset + length - 1,
|
||||
self.md.len()
|
||||
),
|
||||
);
|
||||
} else {
|
||||
res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
|
||||
return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
|
||||
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
|
||||
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
|
||||
};
|
||||
} else {
|
||||
return res.status(StatusCode::BAD_REQUEST).finish();
|
||||
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
|
||||
};
|
||||
};
|
||||
|
||||
if precondition_failed {
|
||||
return res.status(StatusCode::PRECONDITION_FAILED).finish();
|
||||
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
|
||||
} else if not_modified {
|
||||
return res
|
||||
.status(StatusCode::NOT_MODIFIED)
|
||||
.body(body::None::new())
|
||||
.map_into_boxed_body();
|
||||
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
|
||||
}
|
||||
|
||||
let reader = chunked::new_chunked_read(length, offset, self.file);
|
||||
|
||||
if offset != 0 || length != self.md.len() {
|
||||
res.status(StatusCode::PARTIAL_CONTENT);
|
||||
}
|
||||
|
||||
res.body(SizedStream::new(length, reader))
|
||||
}
|
||||
|
||||
/// Returns stream of chunks for the complete file.
|
||||
pub fn into_chunk_stream(self) -> impl Stream<Item = Result<Bytes, Error>> {
|
||||
chunked::new_chunked_read(self.md.len(), 0, self.file)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
|
||||
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 {
|
||||
if item.strong_eq(some_etag) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
|
||||
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 {
|
||||
if item.weak_eq(some_etag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for NamedFile {
|
||||
type Body = BoxBody;
|
||||
|
||||
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
self.into_response(req)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceFactory<ServiceRequest> for NamedFile {
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Config = ();
|
||||
type Service = NamedFileService;
|
||||
type InitError = ();
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let service = NamedFileService {
|
||||
path: self.path.clone(),
|
||||
let reader = ChunkedReadFile {
|
||||
offset,
|
||||
size: length,
|
||||
file: Some(self.file),
|
||||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
|
||||
Box::pin(async move { Ok(service) })
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug)]
|
||||
pub struct NamedFileService {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Service<ServiceRequest> for NamedFileService {
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
dev::always_ready!();
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let (req, _) = req.into_parts();
|
||||
|
||||
let path = self.path.clone();
|
||||
Box::pin(async move {
|
||||
let file = NamedFile::open_async(path).await?;
|
||||
let res = file.into_response(&req);
|
||||
Ok(ServiceResponse::new(req, res))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpServiceFactory for NamedFile {
|
||||
fn register(self, config: &mut AppService) {
|
||||
config.register_service(
|
||||
ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()),
|
||||
None,
|
||||
self,
|
||||
None,
|
||||
)
|
||||
if offset != 0 || length != self.md.len() {
|
||||
return Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader));
|
||||
};
|
||||
Ok(resp.body(SizedStream::new(length, reader)))
|
||||
}
|
||||
}
|
||||
|
@ -1,190 +0,0 @@
|
||||
use std::{
|
||||
path::{Component, Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use actix_utils::future::{ready, Ready};
|
||||
use actix_web::{dev::Payload, FromRequest, HttpRequest};
|
||||
|
||||
use crate::error::UriSegmentError;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
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.
|
||||
///
|
||||
/// Path traversal is guarded by this method.
|
||||
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
||||
let mut buf = PathBuf::new();
|
||||
|
||||
// equivalent to `path.split('/').count()`
|
||||
let mut segment_count = path.matches('/').count() + 1;
|
||||
|
||||
// we can decode the whole path here (instead of per-segment decoding)
|
||||
// because we will reject `%2F` in paths using `segement_count`.
|
||||
let path = percent_encoding::percent_decode_str(path)
|
||||
.decode_utf8()
|
||||
.map_err(|_| UriSegmentError::NotValidUtf8)?;
|
||||
|
||||
// disallow decoding `%2F` into `/`
|
||||
if segment_count != path.matches('/').count() + 1 {
|
||||
return Err(UriSegmentError::BadChar('/'));
|
||||
}
|
||||
|
||||
for segment in path.split('/') {
|
||||
if segment == ".." {
|
||||
segment_count -= 1;
|
||||
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() {
|
||||
segment_count -= 1;
|
||||
continue;
|
||||
} else if cfg!(windows) && segment.contains('\\') {
|
||||
return Err(UriSegmentError::BadChar('\\'));
|
||||
} else if cfg!(windows) && segment.contains(':') {
|
||||
return Err(UriSegmentError::BadChar(':'));
|
||||
} else {
|
||||
buf.push(segment)
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we agree with stdlib parser
|
||||
for (i, component) in buf.components().enumerate() {
|
||||
assert!(
|
||||
matches!(component, Component::Normal(_)),
|
||||
"component `{:?}` is not normal",
|
||||
component
|
||||
);
|
||||
assert!(i < segment_count);
|
||||
}
|
||||
|
||||
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>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(req.match_info().unprocessed().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"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_traversal() {
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/../README.md", false).unwrap().0,
|
||||
PathBuf::from_iter(vec!["README.md"])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/../README.md", true).unwrap().0,
|
||||
PathBuf::from_iter(vec!["README.md"])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false)
|
||||
.unwrap()
|
||||
.0,
|
||||
PathBuf::from_iter(vec!["etc/passwd"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, should_panic)]
|
||||
fn windows_drive_traversal() {
|
||||
// detect issues in windows that could lead to path traversal
|
||||
// see <https://github.com/SergioBenitez/Rocket/issues/1949
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("C:test.txt", false).unwrap().0,
|
||||
PathBuf::from_iter(vec!["C:test.txt"])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("C:../whatever", false).unwrap().0,
|
||||
PathBuf::from_iter(vec!["C:../whatever"])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path(":test.txt", false).unwrap().0,
|
||||
PathBuf::from_iter(vec![":test.txt"])
|
||||
);
|
||||
}
|
||||
}
|
@ -1,35 +1,95 @@
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
#[display(fmt = "Parse HTTP Range failed")]
|
||||
pub struct ParseRangeErr(#[error(not(source))] ());
|
||||
static PREFIX: &str = "bytes=";
|
||||
const PREFIX_LEN: usize = 6;
|
||||
|
||||
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>, ParseRangeErr> {
|
||||
match http_range::HttpRange::parse(header, size) {
|
||||
Ok(ranges) => Ok(ranges
|
||||
.iter()
|
||||
.map(|range| HttpRange {
|
||||
start: range.start,
|
||||
length: range.length,
|
||||
})
|
||||
.collect()),
|
||||
Err(_) => Err(ParseRangeErr(())),
|
||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ()> {
|
||||
if header.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if !header.starts_with(PREFIX) {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let size_sig = size as i64;
|
||||
let mut no_overlap = false;
|
||||
|
||||
let all_ranges: Vec<Option<HttpRange>> = header[PREFIX_LEN..]
|
||||
.split(',')
|
||||
.map(|x| x.trim())
|
||||
.filter(|x| !x.is_empty())
|
||||
.map(|ra| {
|
||||
let mut start_end_iter = ra.split('-');
|
||||
|
||||
let start_str = start_end_iter.next().ok_or(())?.trim();
|
||||
let end_str = start_end_iter.next().ok_or(())?.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(|_| ())?;
|
||||
|
||||
if length > size_sig {
|
||||
length = size_sig;
|
||||
}
|
||||
|
||||
Ok(Some(HttpRange {
|
||||
start: (size_sig - length) as u64,
|
||||
length: length as u64,
|
||||
}))
|
||||
} else {
|
||||
let start: i64 = start_str.parse().map_err(|_| ())?;
|
||||
|
||||
if start < 0 {
|
||||
return Err(());
|
||||
}
|
||||
if start >= size_sig {
|
||||
no_overlap = true;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let length = if end_str.is_empty() {
|
||||
// If no end is specified, range extends to end of the file.
|
||||
size_sig - start
|
||||
} else {
|
||||
let mut end: i64 = end_str.parse().map_err(|_| ())?;
|
||||
|
||||
if start > end {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
if end >= size_sig {
|
||||
end = size_sig - 1;
|
||||
}
|
||||
|
||||
end - start + 1
|
||||
};
|
||||
|
||||
Ok(Some(HttpRange {
|
||||
start: start as u64,
|
||||
length: length as u64,
|
||||
}))
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
|
||||
|
||||
if no_overlap && ranges.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,7 +330,8 @@ mod tests {
|
||||
if expected.is_empty() {
|
||||
continue;
|
||||
} else {
|
||||
panic!(
|
||||
assert!(
|
||||
false,
|
||||
"parse({}, {}) returned error {:?}",
|
||||
header,
|
||||
size,
|
||||
@ -282,24 +343,28 @@ mod tests {
|
||||
let got = res.unwrap();
|
||||
|
||||
if got.len() != expected.len() {
|
||||
panic!(
|
||||
assert!(
|
||||
false,
|
||||
"len(parseRange({}, {})) = {}, want {}",
|
||||
header,
|
||||
size,
|
||||
got.len(),
|
||||
expected.len()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
for i in 0..expected.len() {
|
||||
if got[i].start != expected[i].start {
|
||||
panic!(
|
||||
assert!(
|
||||
false,
|
||||
"parseRange({}, {})[{}].start = {}, want {}",
|
||||
header, size, i, got[i].start, expected[i].start
|
||||
)
|
||||
}
|
||||
if got[i].length != expected[i].length {
|
||||
panic!(
|
||||
assert!(
|
||||
false,
|
||||
"parseRange({}, {})[{}].length = {}, want {}",
|
||||
header, size, i, got[i].length, expected[i].length
|
||||
)
|
||||
|
@ -1,195 +0,0 @@
|
||||
use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
|
||||
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{self, Service, ServiceRequest, ServiceResponse},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::{header, Method},
|
||||
HttpResponse,
|
||||
};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
|
||||
use crate::{
|
||||
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
|
||||
PathBufWrap, PathFilter,
|
||||
};
|
||||
|
||||
/// Assembled file serving service.
|
||||
#[derive(Clone)]
|
||||
pub struct FilesService(pub(crate) Rc<FilesServiceInner>);
|
||||
|
||||
impl Deref for FilesService {
|
||||
type Target = FilesServiceInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FilesServiceInner {
|
||||
pub(crate) directory: PathBuf,
|
||||
pub(crate) 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) path_filter: Option<Rc<PathFilter>>,
|
||||
pub(crate) file_flags: named::Flags,
|
||||
pub(crate) guards: Option<Rc<dyn Guard>>,
|
||||
pub(crate) hidden_files: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for FilesServiceInner {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("FilesServiceInner")
|
||||
}
|
||||
}
|
||||
|
||||
impl FilesService {
|
||||
async fn handle_err(
|
||||
&self,
|
||||
err: io::Error,
|
||||
req: ServiceRequest,
|
||||
) -> Result<ServiceResponse, Error> {
|
||||
log::debug!("error handling {}: {}", req.path(), err);
|
||||
|
||||
if let Some(ref default) = self.default {
|
||||
default.call(req).await
|
||||
} else {
|
||||
Ok(req.error_response(err))
|
||||
}
|
||||
}
|
||||
|
||||
fn serve_named_file(
|
||||
&self,
|
||||
req: ServiceRequest,
|
||||
mut named_file: NamedFile,
|
||||
) -> ServiceResponse {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition = mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
named_file.flags = self.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let res = named_file.into_response(&req);
|
||||
ServiceResponse::new(req, res)
|
||||
}
|
||||
|
||||
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
|
||||
let dir = Directory::new(self.directory.clone(), path);
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
|
||||
(self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FilesService {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("FilesService")
|
||||
}
|
||||
}
|
||||
|
||||
impl Service<ServiceRequest> for FilesService {
|
||||
type Response = ServiceResponse<BoxBody>;
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
dev::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.guard_ctx())
|
||||
} else {
|
||||
// default behavior
|
||||
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||
};
|
||||
|
||||
let this = self.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
if !is_method_valid {
|
||||
return Ok(req.into_response(
|
||||
HttpResponse::MethodNotAllowed()
|
||||
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
|
||||
.body("Request did not meet this resource's requirements."),
|
||||
));
|
||||
}
|
||||
|
||||
let path_on_disk = match PathBufWrap::parse_path(
|
||||
req.match_info().unprocessed(),
|
||||
this.hidden_files,
|
||||
) {
|
||||
Ok(item) => item,
|
||||
Err(err) => return Ok(req.error_response(err)),
|
||||
};
|
||||
|
||||
if let Some(filter) = &this.path_filter {
|
||||
if !filter(path_on_disk.as_ref(), req.head()) {
|
||||
if let Some(ref default) = this.default {
|
||||
return default.call(req).await;
|
||||
} else {
|
||||
return Ok(req.into_response(HttpResponse::NotFound().finish()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// full file path
|
||||
let path = this.directory.join(&path_on_disk);
|
||||
if let Err(err) = path.canonicalize() {
|
||||
return this.handle_err(err, req).await;
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
if this.redirect_to_slash
|
||||
&& !req.path().ends_with('/')
|
||||
&& (this.index.is_some() || this.show_index)
|
||||
{
|
||||
let redirect_to = format!("{}/", req.path());
|
||||
|
||||
return Ok(req.into_response(
|
||||
HttpResponse::Found()
|
||||
.insert_header((header::LOCATION, redirect_to))
|
||||
.finish(),
|
||||
));
|
||||
}
|
||||
|
||||
match this.index {
|
||||
Some(ref index) => {
|
||||
let named_path = path.join(index);
|
||||
match NamedFile::open_async(named_path).await {
|
||||
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
||||
Err(_) if this.show_index => Ok(this.show_index(req, path)),
|
||||
Err(err) => this.handle_err(err, req).await,
|
||||
}
|
||||
}
|
||||
None if this.show_index => Ok(this.show_index(req, path)),
|
||||
None => Ok(ServiceResponse::from_err(
|
||||
FilesError::IsDirectory,
|
||||
req.into_parts().0,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
match NamedFile::open_async(&path).await {
|
||||
Ok(mut named_file) => {
|
||||
if let Some(ref mime_override) = this.mime_override {
|
||||
let new_disposition =
|
||||
mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
named_file.flags = this.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let res = named_file.into_response(&req);
|
||||
Ok(ServiceResponse::new(req, res))
|
||||
}
|
||||
Err(err) => this.handle_err(err, req).await,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
use actix_files::Files;
|
||||
use actix_web::{
|
||||
http::{
|
||||
header::{self, HeaderValue},
|
||||
StatusCode,
|
||||
},
|
||||
test::{self, TestRequest},
|
||||
App,
|
||||
};
|
||||
|
||||
#[actix_web::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; charset=utf-8")),
|
||||
);
|
||||
|
||||
// disable UTF-8 attribute
|
||||
let srv =
|
||||
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false)))
|
||||
.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")),
|
||||
);
|
||||
}
|
@ -1 +0,0 @@
|
||||
first
|
@ -1 +0,0 @@
|
||||
second
|
@ -1,36 +0,0 @@
|
||||
use actix_files::Files;
|
||||
use actix_web::{
|
||||
guard::Host,
|
||||
http::StatusCode,
|
||||
test::{self, TestRequest},
|
||||
App,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_guard_filter() {
|
||||
let srv = test::init_service(
|
||||
App::new()
|
||||
.service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com")))
|
||||
.service(
|
||||
Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::with_uri("/index.txt")
|
||||
.append_header(("Host", "first.com"))
|
||||
.to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(test::read_body(res).await, Bytes::from("first"));
|
||||
|
||||
let req = TestRequest::with_uri("/index.txt")
|
||||
.append_header(("Host", "second.com"))
|
||||
.to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(test::read_body(res).await, Bytes::from("second"));
|
||||
}
|
@ -1 +0,0 @@
|
||||
test.png
|
@ -1 +0,0 @@
|
||||
// this file is empty.
|
@ -1,27 +0,0 @@
|
||||
use actix_files::Files;
|
||||
use actix_web::{
|
||||
http::StatusCode,
|
||||
test::{self, TestRequest},
|
||||
App,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_directory_traversal_prevention() {
|
||||
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||
|
||||
let req =
|
||||
TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri(
|
||||
"/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd",
|
||||
)
|
||||
.to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/%00/etc/passwd%00").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
中文内容显示正确。
|
||||
|
||||
English is OK.
|
38
actix-framed/Cargo.toml
Normal file
38
actix-framed/Cargo.toml
Normal file
@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "actix-framed"
|
||||
version = "0.2.1"
|
||||
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"
|
||||
workspace =".."
|
||||
|
||||
[lib]
|
||||
name = "actix_framed"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.1.2"
|
||||
actix-service = "0.4.1"
|
||||
actix-router = "0.1.2"
|
||||
actix-rt = "0.2.2"
|
||||
actix-http = "0.2.7"
|
||||
actix-server-config = "0.1.2"
|
||||
|
||||
bytes = "0.4"
|
||||
futures = "0.1.25"
|
||||
log = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-server = { version = "0.6.0", features=["ssl"] }
|
||||
actix-connect = { version = "0.2.0", features=["ssl"] }
|
||||
actix-http-test = { version = "0.2.4", features=["ssl"] }
|
||||
actix-utils = "0.4.4"
|
201
actix-framed/LICENSE-APACHE
Normal file
201
actix-framed/LICENSE-APACHE
Normal file
@ -0,0 +1,201 @@
|
||||
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.
|
25
actix-framed/LICENSE-MIT
Normal file
25
actix-framed/LICENSE-MIT
Normal file
@ -0,0 +1,25 @@
|
||||
Copyright (c) 2017 Nikolay Kim
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
8
actix-framed/README.md
Normal file
8
actix-framed/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# 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.33 or later
|
20
actix-framed/changes.md
Normal file
20
actix-framed/changes.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Changes
|
||||
|
||||
## [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
|
217
actix-framed/src/app.rs
Normal file
217
actix-framed/src/app.rs
Normal file
@ -0,0 +1,217 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
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_server_config::ServerConfig;
|
||||
use actix_service::{IntoNewService, NewService, Service};
|
||||
use futures::{Async, Future, Poll};
|
||||
|
||||
use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService};
|
||||
use crate::request::FramedRequest;
|
||||
use crate::state::State;
|
||||
|
||||
type BoxedResponse = Box<dyn Future<Item = (), Error = Error>>;
|
||||
|
||||
pub trait HttpServiceFactory {
|
||||
type Factory: NewService;
|
||||
|
||||
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: NewService<
|
||||
Config = (),
|
||||
Request = FramedRequest<T, S>,
|
||||
Response = (),
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
> + 'static,
|
||||
<U::Factory as NewService>::Future: 'static,
|
||||
<U::Factory as NewService>::Service: Service<
|
||||
Request = FramedRequest<T, S>,
|
||||
Response = (),
|
||||
Error = Error,
|
||||
Future = Box<dyn Future<Item = (), Error = Error>>,
|
||||
>,
|
||||
{
|
||||
let path = factory.path().to_string();
|
||||
self.services
|
||||
.push((path, Box::new(HttpNewService::new(factory.create()))));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> IntoNewService<FramedAppFactory<T, S>> for FramedApp<T, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + 'static,
|
||||
S: 'static,
|
||||
{
|
||||
fn into_new_service(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> NewService for FramedAppFactory<T, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + 'static,
|
||||
S: 'static,
|
||||
{
|
||||
type Config = ServerConfig;
|
||||
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, _: &ServerConfig) -> 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>,
|
||||
Box<dyn Future<Item = BoxedHttpService<FramedRequest<T, S>>, Error = ()>>,
|
||||
),
|
||||
Service(String, BoxedHttpService<FramedRequest<T, S>>),
|
||||
}
|
||||
|
||||
impl<S: 'static, T: 'static> Future for CreateService<T, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
{
|
||||
type Item = FramedAppService<T, S>;
|
||||
type Error = ();
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
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 fut.poll()? {
|
||||
Async::Ready(service) => Some((path.take().unwrap(), service)),
|
||||
Async::NotReady => {
|
||||
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
|
||||
});
|
||||
Ok(Async::Ready(FramedAppService {
|
||||
router: router.finish(),
|
||||
state: self.state.clone(),
|
||||
}))
|
||||
} else {
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
{
|
||||
type Request = (Request, Framed<T, Codec>);
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Future = BoxedResponse;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
Ok(Async::Ready(()))
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
Box::new(
|
||||
SendResponse::new(framed, Response::NotFound().finish()).then(|_| Ok(())),
|
||||
)
|
||||
}
|
||||
}
|
90
actix-framed/src/helpers.rs
Normal file
90
actix-framed/src/helpers.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use actix_http::Error;
|
||||
use actix_service::{NewService, Service};
|
||||
use futures::{Future, Poll};
|
||||
|
||||
pub(crate) type BoxedHttpService<Req> = Box<
|
||||
dyn Service<
|
||||
Request = Req,
|
||||
Response = (),
|
||||
Error = Error,
|
||||
Future = Box<dyn Future<Item = (), Error = Error>>,
|
||||
>,
|
||||
>;
|
||||
|
||||
pub(crate) type BoxedHttpNewService<Req> = Box<
|
||||
dyn NewService<
|
||||
Config = (),
|
||||
Request = Req,
|
||||
Response = (),
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
Service = BoxedHttpService<Req>,
|
||||
Future = Box<dyn Future<Item = BoxedHttpService<Req>, Error = ()>>,
|
||||
>,
|
||||
>;
|
||||
|
||||
pub(crate) struct HttpNewService<T: NewService>(T);
|
||||
|
||||
impl<T> HttpNewService<T>
|
||||
where
|
||||
T: NewService<Response = (), Error = Error>,
|
||||
T::Response: 'static,
|
||||
T::Future: 'static,
|
||||
T::Service: Service<Future = Box<dyn Future<Item = (), Error = Error>>> + 'static,
|
||||
<T::Service as Service>::Future: 'static,
|
||||
{
|
||||
pub fn new(service: T) -> Self {
|
||||
HttpNewService(service)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> NewService for HttpNewService<T>
|
||||
where
|
||||
T: NewService<Config = (), Response = (), Error = Error>,
|
||||
T::Request: 'static,
|
||||
T::Future: 'static,
|
||||
T::Service: Service<Future = Box<dyn Future<Item = (), Error = 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 = Box<dyn Future<Item = Self::Service, Error = ()>>;
|
||||
|
||||
fn new_service(&self, _: &()) -> Self::Future {
|
||||
Box::new(self.0.new_service(&()).map_err(|_| ()).and_then(|service| {
|
||||
let service: BoxedHttpService<_> = Box::new(HttpServiceWrapper { service });
|
||||
Ok(service)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpServiceWrapper<T: Service> {
|
||||
service: T,
|
||||
}
|
||||
|
||||
impl<T> Service for HttpServiceWrapper<T>
|
||||
where
|
||||
T: Service<
|
||||
Response = (),
|
||||
Future = Box<dyn Future<Item = (), Error = Error>>,
|
||||
Error = Error,
|
||||
>,
|
||||
T::Request: 'static,
|
||||
{
|
||||
type Request = T::Request;
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Future = Box<dyn Future<Item = (), Error = Error>>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
self.service.poll_ready()
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Self::Request) -> Self::Future {
|
||||
self.service.call(req)
|
||||
}
|
||||
}
|
17
actix-framed/src/lib.rs
Normal file
17
actix-framed/src/lib.rs
Normal file
@ -0,0 +1,17 @@
|
||||
#![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 convinience
|
||||
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;
|
170
actix-framed/src/request.rs
Normal file
170
actix-framed/src/request.rs
Normal file
@ -0,0 +1,170 @@
|
||||
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 muttable 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 actix_http::http::{HeaderName, HeaderValue, HttpTryFrom};
|
||||
use actix_http::test::{TestBuffer, TestRequest};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_reqest() {
|
||||
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);
|
||||
}
|
||||
}
|
157
actix-framed/src/route.rs
Normal file
157
actix-framed/src/route.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_http::{http::Method, Error};
|
||||
use actix_service::{NewService, Service};
|
||||
use futures::future::{ok, FutureResult};
|
||||
use futures::{Async, Future, IntoFuture, Poll};
|
||||
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 = ()> {
|
||||
handler: F,
|
||||
pattern: String,
|
||||
methods: Vec<Method>,
|
||||
state: PhantomData<(Io, S, R)>,
|
||||
}
|
||||
|
||||
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>(self, handler: F) -> FramedRoute<Io, S, F, R>
|
||||
where
|
||||
F: FnMut(FramedRequest<Io, S>) -> R,
|
||||
R: IntoFuture<Item = ()>,
|
||||
R::Future: 'static,
|
||||
R::Error: fmt::Debug,
|
||||
{
|
||||
FramedRoute {
|
||||
handler,
|
||||
pattern: self.pattern,
|
||||
methods: self.methods,
|
||||
state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io, S, F, R> HttpServiceFactory for FramedRoute<Io, S, F, R>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + 'static,
|
||||
F: FnMut(FramedRequest<Io, S>) -> R + Clone,
|
||||
R: IntoFuture<Item = ()>,
|
||||
R::Future: 'static,
|
||||
R::Error: fmt::Display,
|
||||
{
|
||||
type Factory = FramedRouteFactory<Io, S, F, R>;
|
||||
|
||||
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> {
|
||||
handler: F,
|
||||
methods: Vec<Method>,
|
||||
_t: PhantomData<(Io, S, R)>,
|
||||
}
|
||||
|
||||
impl<Io, S, F, R> NewService for FramedRouteFactory<Io, S, F, R>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + 'static,
|
||||
F: FnMut(FramedRequest<Io, S>) -> R + Clone,
|
||||
R: IntoFuture<Item = ()>,
|
||||
R::Future: 'static,
|
||||
R::Error: fmt::Display,
|
||||
{
|
||||
type Config = ();
|
||||
type Request = FramedRequest<Io, S>;
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type InitError = ();
|
||||
type Service = FramedRouteService<Io, S, F, R>;
|
||||
type Future = FutureResult<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> {
|
||||
handler: F,
|
||||
methods: Vec<Method>,
|
||||
_t: PhantomData<(Io, S, R)>,
|
||||
}
|
||||
|
||||
impl<Io, S, F, R> Service for FramedRouteService<Io, S, F, R>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + 'static,
|
||||
F: FnMut(FramedRequest<Io, S>) -> R + Clone,
|
||||
R: IntoFuture<Item = ()>,
|
||||
R::Future: 'static,
|
||||
R::Error: fmt::Display,
|
||||
{
|
||||
type Request = FramedRequest<Io, S>;
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Future = Box<dyn Future<Item = (), Error = Error>>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
Ok(Async::Ready(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: FramedRequest<Io, S>) -> Self::Future {
|
||||
Box::new((self.handler)(req).into_future().then(|res| {
|
||||
if let Err(e) = res {
|
||||
error!("Error in request handler: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
}
|
149
actix-framed/src/service.rs
Normal file
149
actix-framed/src/service.rs
Normal file
@ -0,0 +1,149 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
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::{NewService, Service};
|
||||
use futures::future::{ok, Either, FutureResult};
|
||||
use futures::{Async, Future, IntoFuture, Poll, Sink};
|
||||
|
||||
/// 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> NewService 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 = FutureResult<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 = FutureResult<Self::Response, Self::Error>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
Ok(Async::Ready(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, (req, framed): (Request, Framed<T, Codec>)) -> Self::Future {
|
||||
match verify_handshake(req.head()) {
|
||||
Err(e) => Err((e, framed)).into_future(),
|
||||
Ok(_) => Ok((req, framed)).into_future(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> NewService for SendError<T, R, E, C>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + '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 = FutureResult<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 + '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<FutureResult<R, (E, Framed<T, Codec>)>, SendErrorFut<T, R, E>>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
Ok(Async::Ready(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Result<R, (E, Framed<T, Codec>)>) -> Self::Future {
|
||||
match req {
|
||||
Ok(r) => Either::A(ok(r)),
|
||||
Err((e, framed)) => {
|
||||
let res = e.error_response().drop_body();
|
||||
Either::B(SendErrorFut {
|
||||
framed: Some(framed),
|
||||
res: Some((res, BodySize::Empty).into()),
|
||||
err: Some(e),
|
||||
_t: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
{
|
||||
type Item = R;
|
||||
type Error = (E, Framed<T, Codec>);
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
if let Some(res) = self.res.take() {
|
||||
if self.framed.as_mut().unwrap().force_send(res).is_err() {
|
||||
return Err((self.err.take().unwrap(), self.framed.take().unwrap()));
|
||||
}
|
||||
}
|
||||
match self.framed.as_mut().unwrap().poll_complete() {
|
||||
Ok(Async::Ready(_)) => {
|
||||
Err((self.err.take().unwrap(), self.framed.take().unwrap()))
|
||||
}
|
||||
Ok(Async::NotReady) => Ok(Async::NotReady),
|
||||
Err(_) => Err((self.err.take().unwrap(), self.framed.take().unwrap())),
|
||||
}
|
||||
}
|
||||
}
|
29
actix-framed/src/state.rs
Normal file
29
actix-framed/src/state.rs
Normal file
@ -0,0 +1,29 @@
|
||||
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())
|
||||
}
|
||||
}
|
153
actix-framed/src/test.rs
Normal file
153
actix-framed/src/test.rs
Normal file
@ -0,0 +1,153 @@
|
||||
//! Various helpers for Actix applications to use during testing.
|
||||
use actix_codec::Framed;
|
||||
use actix_http::h1::Codec;
|
||||
use actix_http::http::header::{Header, HeaderName, IntoHeaderValue};
|
||||
use actix_http::http::{HttpTryFrom, Method, Uri, Version};
|
||||
use actix_http::test::{TestBuffer, TestRequest as HttpTestRequest};
|
||||
use actix_router::{Path, Url};
|
||||
use actix_rt::Runtime;
|
||||
use futures::IntoFuture;
|
||||
|
||||
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: HttpTryFrom<K>,
|
||||
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: HttpTryFrom<K>,
|
||||
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 fn run<F, R, I, E>(self, f: F) -> Result<I, E>
|
||||
where
|
||||
F: FnOnce(FramedRequest<TestBuffer, S>) -> R,
|
||||
R: IntoFuture<Item = I, Error = E>,
|
||||
{
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
rt.block_on(f(self.finish()).into_future())
|
||||
}
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
141
actix-framed/tests/test_server.rs
Normal file
141
actix-framed/tests/test_server.rs
Normal file
@ -0,0 +1,141 @@
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_http::{body, http::StatusCode, ws, Error, HttpService, Response};
|
||||
use actix_http_test::TestServer;
|
||||
use actix_service::{IntoNewService, NewService};
|
||||
use actix_utils::framed::FramedTransport;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::future::{self, ok};
|
||||
use futures::{Future, Sink, Stream};
|
||||
|
||||
use actix_framed::{FramedApp, FramedRequest, FramedRoute, SendError, VerifyWebSockets};
|
||||
|
||||
fn ws_service<T: AsyncRead + AsyncWrite>(
|
||||
req: FramedRequest<T>,
|
||||
) -> impl Future<Item = (), Error = Error> {
|
||||
let (req, framed, _) = req.into_parts();
|
||||
let res = ws::handshake(req.head()).unwrap().message_body(());
|
||||
|
||||
framed
|
||||
.send((res, body::BodySize::None).into())
|
||||
.map_err(|_| panic!())
|
||||
.and_then(|framed| {
|
||||
FramedTransport::new(framed.into_framed(ws::Codec::new()), service)
|
||||
.map_err(|_| panic!())
|
||||
})
|
||||
}
|
||||
|
||||
fn service(msg: ws::Frame) -> impl Future<Item = ws::Message, Error = 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.unwrap()).to_string())
|
||||
}
|
||||
ws::Frame::Binary(bin) => ws::Message::Binary(bin.unwrap().freeze()),
|
||||
ws::Frame::Close(reason) => ws::Message::Close(reason),
|
||||
_ => panic!(),
|
||||
};
|
||||
ok(msg)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let mut srv = TestServer::new(|| {
|
||||
HttpService::build()
|
||||
.upgrade(
|
||||
FramedApp::new().service(FramedRoute::get("/index.html").to(ws_service)),
|
||||
)
|
||||
.finish(|_| future::ok::<_, Error>(Response::NotFound()))
|
||||
});
|
||||
|
||||
assert!(srv.ws_at("/test").is_err());
|
||||
|
||||
// client service
|
||||
let framed = srv.ws_at("/index.html").unwrap();
|
||||
let framed = srv
|
||||
.block_on(framed.send(ws::Message::Text("text".to_string())))
|
||||
.unwrap();
|
||||
let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap();
|
||||
assert_eq!(item, Some(ws::Frame::Text(Some(BytesMut::from("text")))));
|
||||
|
||||
let framed = srv
|
||||
.block_on(framed.send(ws::Message::Binary("text".into())))
|
||||
.unwrap();
|
||||
let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap();
|
||||
assert_eq!(
|
||||
item,
|
||||
Some(ws::Frame::Binary(Some(Bytes::from_static(b"text").into())))
|
||||
);
|
||||
|
||||
let framed = srv
|
||||
.block_on(framed.send(ws::Message::Ping("text".into())))
|
||||
.unwrap();
|
||||
let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap();
|
||||
assert_eq!(item, Some(ws::Frame::Pong("text".to_string().into())));
|
||||
|
||||
let framed = srv
|
||||
.block_on(framed.send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))))
|
||||
.unwrap();
|
||||
|
||||
let (item, _framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap();
|
||||
assert_eq!(
|
||||
item,
|
||||
Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service() {
|
||||
let mut srv = TestServer::new(|| {
|
||||
actix_http::h1::OneRequest::new().map_err(|_| ()).and_then(
|
||||
VerifyWebSockets::default()
|
||||
.then(SendError::default())
|
||||
.map_err(|_| ())
|
||||
.and_then(
|
||||
FramedApp::new()
|
||||
.service(FramedRoute::get("/index.html").to(ws_service))
|
||||
.into_new_service()
|
||||
.map_err(|_| ()),
|
||||
),
|
||||
)
|
||||
});
|
||||
|
||||
// non ws request
|
||||
let res = srv.block_on(srv.get("/index.html").send()).unwrap();
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
// not found
|
||||
assert!(srv.ws_at("/test").is_err());
|
||||
|
||||
// client service
|
||||
let framed = srv.ws_at("/index.html").unwrap();
|
||||
let framed = srv
|
||||
.block_on(framed.send(ws::Message::Text("text".to_string())))
|
||||
.unwrap();
|
||||
let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap();
|
||||
assert_eq!(item, Some(ws::Frame::Text(Some(BytesMut::from("text")))));
|
||||
|
||||
let framed = srv
|
||||
.block_on(framed.send(ws::Message::Binary("text".into())))
|
||||
.unwrap();
|
||||
let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap();
|
||||
assert_eq!(
|
||||
item,
|
||||
Some(ws::Frame::Binary(Some(Bytes::from_static(b"text").into())))
|
||||
);
|
||||
|
||||
let framed = srv
|
||||
.block_on(framed.send(ws::Message::Ping("text".into())))
|
||||
.unwrap();
|
||||
let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap();
|
||||
assert_eq!(item, Some(ws::Frame::Pong("text".to_string().into())));
|
||||
|
||||
let framed = srv
|
||||
.block_on(framed.send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))))
|
||||
.unwrap();
|
||||
|
||||
let (item, _framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap();
|
||||
assert_eq!(
|
||||
item,
|
||||
Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into())))
|
||||
);
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.13 - 2022-02-16
|
||||
- No significant changes since `3.0.0-beta.12`.
|
||||
|
||||
|
||||
## 3.0.0-beta.12 - 2022-01-31
|
||||
- No significant changes since `3.0.0-beta.11`.
|
||||
|
||||
|
||||
## 3.0.0-beta.11 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 3.0.0-beta.10 - 2021-12-27
|
||||
- Update `actix-server` to `2.0.0-rc.2`. [#2550]
|
||||
|
||||
[#2550]: https://github.com/actix/actix-web/pull/2550
|
||||
|
||||
|
||||
## 3.0.0-beta.9 - 2021-12-11
|
||||
- No significant changes since `3.0.0-beta.8`.
|
||||
|
||||
|
||||
## 3.0.0-beta.8 - 2021-11-30
|
||||
- Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
|
||||
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||
|
||||
|
||||
## 3.0.0-beta.7 - 2021-11-22
|
||||
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||
|
||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||
|
||||
|
||||
## 3.0.0-beta.6 - 2021-11-15
|
||||
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
||||
- Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||
|
||||
|
||||
## 3.0.0-beta.5 - 2021-09-09
|
||||
- Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
||||
|
||||
## 3.0.0-beta.4 - 2021-04-02
|
||||
- Added `TestServer::client_headers` method. [#2097]
|
||||
|
||||
[#2097]: https://github.com/actix/actix-web/pull/2097
|
||||
|
||||
|
||||
## 3.0.0-beta.3 - 2021-03-09
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2021-02-10
|
||||
- No notable changes.
|
||||
|
||||
|
||||
## 3.0.0-beta.1 - 2021-01-07
|
||||
- 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.
|
||||
- Bump minimum supported Rust version to 1.40
|
||||
- Replace deprecated `net2` crate with `socket2`
|
||||
- Update `base64` dependency to 0.12
|
||||
- Update `env_logger` dependency to 0.7
|
||||
|
||||
## 1.0.0 - 2019-12-13
|
||||
- Replaced `TestServer::start()` with `test_server()`
|
||||
|
||||
|
||||
## 1.0.0-alpha.3 - 2019-12-07
|
||||
- Migrate to `std::future`
|
||||
|
||||
|
||||
## 0.2.5 - 2019-09-17
|
||||
- Update serde_urlencoded to "0.6.1"
|
||||
- Increase TestServerRuntime timeouts from 500ms to 3000ms
|
||||
- Do not override current `System`
|
||||
|
||||
|
||||
## 0.2.4 - 2019-07-18
|
||||
- Update actix-server to 0.6
|
||||
|
||||
|
||||
## 0.2.3 - 2019-07-16
|
||||
- Add `delete`, `options`, `patch` methods to `TestServerRunner`
|
||||
|
||||
|
||||
## 0.2.2 - 2019-06-16
|
||||
- Add .put() and .sput() methods
|
||||
|
||||
|
||||
## 0.2.1 - 2019-06-05
|
||||
- Add license files
|
||||
|
||||
|
||||
## 0.2.0 - 2019-05-12
|
||||
- Update awc and actix-http deps
|
||||
|
||||
|
||||
## 0.1.1 - 2019-04-24
|
||||
- Always make new connection for http client
|
||||
|
||||
|
||||
## 0.1.0 - 2019-04-16
|
||||
- No changes
|
||||
|
||||
|
||||
## 0.1.0-alpha.3 - 2019-04-02
|
||||
- Request functions accept path #743
|
||||
|
||||
|
||||
## 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
|
||||
- Initial impl
|
@ -1,55 +0,0 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0-beta.13"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
categories = [
|
||||
"network-programming",
|
||||
"asynchronous",
|
||||
"web-programming::http-server",
|
||||
"web-programming::websocket",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
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"
|
||||
actix-codec = "0.5"
|
||||
actix-tls = "3"
|
||||
actix-utils = "3.0.0"
|
||||
actix-rt = "2.2"
|
||||
actix-server = "2"
|
||||
awc = { version = "3.0.0-beta.21", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
http = "0.2.5"
|
||||
log = "0.4"
|
||||
socket2 = "0.4"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0"
|
@ -1,17 +0,0 @@
|
||||
# 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/3.0.0-beta.13)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br>
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.13)
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http-test)
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
@ -1,312 +0,0 @@
|
||||
//! Various helpers for Actix applications to use during testing.
|
||||
|
||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||
#![warn(future_incompatible)]
|
||||
#![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::{net, thread, time::Duration};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_rt::{net::TcpStream, System};
|
||||
use actix_server::{Server, ServerServiceFactory};
|
||||
use awc::{
|
||||
error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse,
|
||||
Connector,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_core::stream::Stream;
|
||||
use http::Method;
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Start test server.
|
||||
///
|
||||
/// `TestServer` is very simple test server that simplify process of writing integration tests cases
|
||||
/// for HTTP applications.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```no_run
|
||||
/// use actix_http::HttpService;
|
||||
/// use actix_http_test::test_server;
|
||||
/// use actix_web::{web, App, HttpResponse, Error};
|
||||
///
|
||||
/// async fn my_handler() -> Result<HttpResponse, Error> {
|
||||
/// Ok(HttpResponse::Ok().into())
|
||||
/// }
|
||||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_example() {
|
||||
/// let mut srv = TestServer::start(||
|
||||
/// HttpService::new(
|
||||
/// App::new().service(web::resource("/").to(my_handler))
|
||||
/// )
|
||||
/// );
|
||||
///
|
||||
/// let req = srv.get("/");
|
||||
/// let response = req.send().await.unwrap();
|
||||
/// assert!(response.status().is_success());
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn test_server<F: ServerServiceFactory<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 an existing address binding.
|
||||
pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
|
||||
tcp: net::TcpListener,
|
||||
factory: F,
|
||||
) -> TestServer {
|
||||
let (started_tx, started_rx) = std::sync::mpsc::channel();
|
||||
let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1);
|
||||
|
||||
// run server in separate thread
|
||||
thread::spawn(move || {
|
||||
System::new().block_on(async move {
|
||||
let local_addr = tcp.local_addr().unwrap();
|
||||
|
||||
let srv = Server::build()
|
||||
.workers(1)
|
||||
.disable_signals()
|
||||
.system_exit()
|
||||
.listen("test", tcp, factory)
|
||||
.expect("test server could not be created");
|
||||
|
||||
let srv = srv.run();
|
||||
started_tx
|
||||
.send((System::current(), srv.handle(), local_addr))
|
||||
.unwrap();
|
||||
|
||||
// drive server loop
|
||||
srv.await.unwrap();
|
||||
});
|
||||
|
||||
// notify TestServer that server and system have shut down
|
||||
// all thread managed resources should be dropped at this point
|
||||
let _ = thread_stop_tx.send(());
|
||||
});
|
||||
|
||||
let (system, server, addr) = started_rx.recv().unwrap();
|
||||
|
||||
let client = {
|
||||
#[cfg(feature = "openssl")]
|
||||
let connector = {
|
||||
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||
|
||||
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
|
||||
builder.set_verify(SslVerifyMode::NONE);
|
||||
let _ = builder
|
||||
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
||||
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
||||
|
||||
Connector::new()
|
||||
.conn_lifetime(Duration::from_secs(0))
|
||||
.timeout(Duration::from_millis(30000))
|
||||
.openssl(builder.build())
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "openssl"))]
|
||||
let connector = {
|
||||
Connector::new()
|
||||
.conn_lifetime(Duration::from_secs(0))
|
||||
.timeout(Duration::from_millis(30000))
|
||||
};
|
||||
|
||||
Client::builder().connector(connector).finish()
|
||||
};
|
||||
|
||||
TestServer {
|
||||
server,
|
||||
client,
|
||||
system,
|
||||
addr,
|
||||
thread_stop_rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Test server controller
|
||||
pub struct TestServer {
|
||||
server: actix_server::ServerHandle,
|
||||
client: awc::Client,
|
||||
system: actix_rt::System,
|
||||
addr: net::SocketAddr,
|
||||
thread_stop_rx: mpsc::Receiver<()>,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
/// Construct test server url
|
||||
pub fn addr(&self) -> net::SocketAddr {
|
||||
self.addr
|
||||
}
|
||||
|
||||
/// Construct test server url
|
||||
pub fn url(&self, uri: &str) -> String {
|
||||
if uri.starts_with('/') {
|
||||
format!("http://localhost:{}{}", self.addr.port(), uri)
|
||||
} else {
|
||||
format!("http://localhost:{}/{}", self.addr.port(), uri)
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct test HTTPS server URL.
|
||||
pub fn surl(&self, uri: &str) -> String {
|
||||
if uri.starts_with('/') {
|
||||
format!("https://localhost:{}{}", self.addr.port(), uri)
|
||||
} else {
|
||||
format!("https://localhost:{}/{}", self.addr.port(), uri)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `GET` request
|
||||
pub fn get<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.get(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create HTTPS `GET` request
|
||||
pub fn sget<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.get(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create `POST` request
|
||||
pub fn post<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.post(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create HTTPS `POST` request
|
||||
pub fn spost<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.post(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create `HEAD` request
|
||||
pub fn head<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.head(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create HTTPS `HEAD` request
|
||||
pub fn shead<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.head(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create `PUT` request
|
||||
pub fn put<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.put(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create HTTPS `PUT` request
|
||||
pub fn sput<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.put(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create `PATCH` request
|
||||
pub fn patch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.patch(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create HTTPS `PATCH` request
|
||||
pub fn spatch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.patch(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create `DELETE` request
|
||||
pub fn delete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.delete(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create HTTPS `DELETE` request
|
||||
pub fn sdelete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.delete(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create `OPTIONS` request
|
||||
pub fn options<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.options(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create HTTPS `OPTIONS` request
|
||||
pub fn soptions<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.options(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Connect to test HTTP server
|
||||
pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
|
||||
self.client.request(method, path.as_ref())
|
||||
}
|
||||
|
||||
pub async fn load_body<S>(
|
||||
&mut self,
|
||||
mut response: ClientResponse<S>,
|
||||
) -> Result<Bytes, PayloadError>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static,
|
||||
{
|
||||
response.body().limit(10_485_760).await
|
||||
}
|
||||
|
||||
/// Connect to WebSocket server at a given path.
|
||||
pub async fn ws_at(
|
||||
&mut self,
|
||||
path: &str,
|
||||
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
|
||||
let url = self.url(path);
|
||||
let connect = self.client.ws(url).connect();
|
||||
connect.await.map(|(_, framed)| framed)
|
||||
}
|
||||
|
||||
/// Connect to a WebSocket server.
|
||||
pub async fn ws(
|
||||
&mut self,
|
||||
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
|
||||
self.ws_at("/").await
|
||||
}
|
||||
|
||||
/// Get default HeaderMap of Client.
|
||||
///
|
||||
/// Returns Some(&mut HeaderMap) when Client object is unique
|
||||
/// (No other clone of client exists at the same time).
|
||||
pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
|
||||
self.client.headers()
|
||||
}
|
||||
|
||||
/// Stop HTTP server.
|
||||
///
|
||||
/// Waits for spawned `Server` and `System` to (force) shutdown.
|
||||
pub async fn stop(&mut self) {
|
||||
// signal server to stop
|
||||
self.server.stop(false).await;
|
||||
|
||||
// also signal system to stop
|
||||
// though this is handled by `ServerBuilder::exit_system` too
|
||||
self.system.stop();
|
||||
|
||||
// wait for thread to be stopped but don't care about result
|
||||
let _ = self.thread_stop_rx.recv().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestServer {
|
||||
fn drop(&mut self) {
|
||||
// calls in this Drop impl should be enough to shut down the server, system, and thread
|
||||
// without needing to await anything
|
||||
|
||||
// signal server to stop
|
||||
let _ = self.server.stop(true);
|
||||
|
||||
// signal system to stop
|
||||
self.system.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a localhost socket address with random, unused port.
|
||||
pub fn unused_addr() -> net::SocketAddr {
|
||||
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
|
||||
socket.bind(&addr.into()).unwrap();
|
||||
socket.set_reuse_address(true).unwrap();
|
||||
let tcp = net::TcpListener::from(socket);
|
||||
tcp.local_addr().unwrap()
|
||||
}
|
41
actix-http/.appveyor.yml
Normal file
41
actix-http/.appveyor.yml
Normal file
@ -0,0 +1,41 @@
|
||||
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
|
File diff suppressed because it is too large
Load Diff
46
actix-http/CODE_OF_CONDUCT.md
Normal file
46
actix-http/CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fafhrd91@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
@ -1,26 +1,22 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.0.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
]
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
version = "0.2.11"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix http primitives"
|
||||
readme = "README.md"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
categories = [
|
||||
"network-programming",
|
||||
"asynchronous",
|
||||
"web-programming::http-server",
|
||||
"web-programming::websocket",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
documentation = "https://docs.rs/actix-http/"
|
||||
categories = ["network-programming", "asynchronous",
|
||||
"web-programming::http-server",
|
||||
"web-programming::websocket"]
|
||||
license = "MIT/Apache-2.0"
|
||||
edition = "2018"
|
||||
workspace = ".."
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
# features that docs.rs will build with
|
||||
features = ["http2", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
||||
features = ["ssl", "fail", "brotli", "flate2-zlib", "secure-cookies"]
|
||||
|
||||
[lib]
|
||||
name = "actix_http"
|
||||
@ -29,111 +25,86 @@ path = "src/lib.rs"
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# HTTP/2 protocol support
|
||||
http2 = ["h2"]
|
||||
# openssl
|
||||
ssl = ["openssl", "actix-connect/ssl"]
|
||||
|
||||
# WebSocket protocol implementation
|
||||
ws = [
|
||||
"local-channel",
|
||||
"base64",
|
||||
"rand",
|
||||
"sha-1",
|
||||
]
|
||||
# rustls support
|
||||
rust-tls = ["rustls", "webpki-roots", "actix-connect/rust-tls"]
|
||||
|
||||
# TLS via OpenSSL
|
||||
openssl = ["actix-tls/accept", "actix-tls/openssl"]
|
||||
# brotli encoding, requires c compiler
|
||||
brotli = ["brotli2"]
|
||||
|
||||
# TLS via Rustls
|
||||
rustls = ["actix-tls/accept", "actix-tls/rustls"]
|
||||
# miniz-sys backend for flate2 crate
|
||||
flate2-zlib = ["flate2/miniz-sys"]
|
||||
|
||||
# Compression codecs
|
||||
compress-brotli = ["__compress", "brotli"]
|
||||
compress-gzip = ["__compress", "flate2"]
|
||||
compress-zstd = ["__compress", "zstd"]
|
||||
# rust backend for flate2 crate
|
||||
flate2-rust = ["flate2/rust_backend"]
|
||||
|
||||
# Internal (PRIVATE!) features used to aid testing and cheking feature status.
|
||||
# Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime.
|
||||
__compress = []
|
||||
# failure integration. actix does not use failure anymore
|
||||
fail = ["failure"]
|
||||
|
||||
# support for secure cookies
|
||||
secure-cookies = ["ring"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2"
|
||||
actix-codec = "0.5"
|
||||
actix-utils = "3"
|
||||
actix-rt = { version = "2.2", default-features = false }
|
||||
actix-service = "0.4.1"
|
||||
actix-codec = "0.1.2"
|
||||
actix-connect = "0.2.4"
|
||||
actix-utils = "0.4.4"
|
||||
actix-server-config = "0.1.2"
|
||||
actix-threadpool = "0.1.1"
|
||||
|
||||
ahash = "0.7"
|
||||
bitflags = "1.2"
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
derive_more = "0.99.5"
|
||||
base64 = "0.10"
|
||||
bitflags = "1.0"
|
||||
bytes = "0.4"
|
||||
copyless = "0.1.4"
|
||||
derive_more = "0.15.0"
|
||||
either = "1.5.2"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.5"
|
||||
httparse = "1.5.1"
|
||||
httpdate = "1.0.1"
|
||||
itoa = "1"
|
||||
language-tags = "0.3"
|
||||
futures = "0.1.25"
|
||||
hashbrown = "0.6.3"
|
||||
h2 = "0.1.16"
|
||||
http = "0.1.17"
|
||||
httparse = "1.3"
|
||||
indexmap = "1.2"
|
||||
lazy_static = "1.0"
|
||||
language-tags = "0.2"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2"
|
||||
smallvec = "1.6.1"
|
||||
rand = "0.7"
|
||||
regex = "1.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
sha1 = "0.6"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.6.1"
|
||||
time = "0.1.42"
|
||||
tokio-tcp = "0.1.3"
|
||||
tokio-timer = "0.2.8"
|
||||
tokio-current-thread = "0.1"
|
||||
trust-dns-resolver = { version="0.11.1", default-features = false }
|
||||
|
||||
# http2
|
||||
h2 = { version = "0.3.9", optional = true }
|
||||
# for secure cookie
|
||||
ring = { version = "0.14.6", optional = true }
|
||||
|
||||
# websockets
|
||||
local-channel = { version = "0.1", optional = true }
|
||||
base64 = { version = "0.13", optional = true }
|
||||
rand = { version = "0.8", optional = true }
|
||||
sha-1 = { version = "0.10", optional = true }
|
||||
# compression
|
||||
brotli2 = { version="0.3.2", optional = true }
|
||||
flate2 = { version="1.0.7", optional = true, default-features = false }
|
||||
|
||||
# openssl/rustls
|
||||
actix-tls = { version = "3", default-features = false, optional = true }
|
||||
|
||||
# compress-*
|
||||
brotli = { version = "3.3.3", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
zstd = { version = "0.10", optional = true }
|
||||
# optional deps
|
||||
failure = { version = "0.1.5", optional = true }
|
||||
openssl = { version="0.10", optional = true }
|
||||
rustls = { version = "0.15.2", optional = true }
|
||||
webpki-roots = { version = "0.16", optional = true }
|
||||
chrono = "0.4.6"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
|
||||
actix-server = "2"
|
||||
actix-tls = { version = "3", features = ["openssl"] }
|
||||
actix-web = "4.0.0"
|
||||
|
||||
async-stream = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
memchr = "2.4"
|
||||
once_cell = "1.9"
|
||||
rcgen = "0.8"
|
||||
regex = "1.3"
|
||||
rustls-pemfile = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
||||
|
||||
[[example]]
|
||||
name = "ws"
|
||||
required-features = ["rustls"]
|
||||
|
||||
[[bench]]
|
||||
name = "write-camel-case"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "status-line"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "uninit-headers"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "quality-value"
|
||||
harness = false
|
||||
actix-rt = "0.2.2"
|
||||
actix-server = { version = "0.6.0", features=["ssl", "rust-tls"] }
|
||||
actix-connect = { version = "0.2.0", features=["ssl"] }
|
||||
actix-http-test = { version = "0.2.4", features=["ssl"] }
|
||||
env_logger = "0.6"
|
||||
serde_derive = "1.0"
|
||||
openssl = { version="0.10" }
|
||||
tokio-tcp = "0.1"
|
||||
|
@ -1 +0,0 @@
|
||||
../LICENSE-APACHE
|
201
actix-http/LICENSE-APACHE
Normal file
201
actix-http/LICENSE-APACHE
Normal file
@ -0,0 +1,201 @@
|
||||
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 +0,0 @@
|
||||
../LICENSE-MIT
|
25
actix-http/LICENSE-MIT
Normal file
25
actix-http/LICENSE-MIT
Normal file
@ -0,0 +1,25 @@
|
||||
Copyright (c) 2017 Nikolay Kim
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
@ -1,52 +1,32 @@
|
||||
# actix-http
|
||||
# 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)
|
||||
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
Actix http
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.0.0)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
## Documentation & community resources
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http)
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
* [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.31 or later
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
use std::{env, io};
|
||||
// see examples/framed_hello.rs for complete list of used crates.
|
||||
extern crate actix_http;
|
||||
use actix_http::{h1, Response, ServiceConfig};
|
||||
|
||||
use actix_http::{HttpService, Response};
|
||||
use actix_server::Server;
|
||||
use futures_util::future;
|
||||
use http::header::HeaderValue;
|
||||
use log::info;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env::set_var("RUST_LOG", "hello_world=info");
|
||||
env_logger::init();
|
||||
|
||||
Server::build()
|
||||
.bind("hello-world", "127.0.0.1:8080", || {
|
||||
HttpService::build()
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.finish(|_req| {
|
||||
info!("{:?}", _req);
|
||||
let mut res = Response::Ok();
|
||||
res.header("x-head", HeaderValue::from_static("dummy value!"));
|
||||
future::ok::<_, ()>(res.body("Hello world!"))
|
||||
})
|
||||
.tcp()
|
||||
})?
|
||||
.run()
|
||||
.await
|
||||
fn main() {
|
||||
Server::new().bind("framed_hello", "127.0.0.1:8080", || {
|
||||
IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) // <- create h1 codec
|
||||
.and_then(TakeItem::new().map_err(|_| ())) // <- read one request
|
||||
.and_then(|(_req, _framed): (_, Framed<_, _>)| { // <- send response and close conn
|
||||
SendResponse::send(_framed, Response::Ok().body("Hello world!"))
|
||||
.map_err(|_| ())
|
||||
.map(|_| ())
|
||||
})
|
||||
}).unwrap().run();
|
||||
}
|
||||
```
|
||||
|
||||
@ -54,8 +34,8 @@ async fn main() -> io::Result<()> {
|
||||
|
||||
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](http://www.apache.org/licenses/LICENSE-2.0))
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||
|
||||
at your option.
|
||||
|
||||
|
@ -1,95 +0,0 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||
|
||||
fn bench_quality_display_impls(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("quality value display impls");
|
||||
|
||||
for i in CODES.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
|
||||
b.iter(|| _new::Quality(i).to_string())
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||
b.iter(|| _naive::Quality(i).to_string())
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_quality_display_impls);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _new {
|
||||
use std::fmt;
|
||||
|
||||
pub struct Quality(pub(crate) u16);
|
||||
|
||||
impl fmt::Display for Quality {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
0 => f.write_str("0"),
|
||||
1000 => f.write_str("1"),
|
||||
|
||||
// some number in the range 1–999
|
||||
x => {
|
||||
f.write_str("0.")?;
|
||||
|
||||
// this implementation avoids string allocation otherwise required
|
||||
// for `.trim_end_matches('0')`
|
||||
|
||||
if x < 10 {
|
||||
f.write_str("00")?;
|
||||
// 0 is handled so it's not possible to have a trailing 0, we can just return
|
||||
itoa_fmt(f, x)
|
||||
} else if x < 100 {
|
||||
f.write_str("0")?;
|
||||
if x % 10 == 0 {
|
||||
// trailing 0, divide by 10 and write
|
||||
itoa_fmt(f, x / 10)
|
||||
} else {
|
||||
itoa_fmt(f, x)
|
||||
}
|
||||
} else {
|
||||
// x is in range 101–999
|
||||
|
||||
if x % 100 == 0 {
|
||||
// two trailing 0s, divide by 100 and write
|
||||
itoa_fmt(f, x / 100)
|
||||
} else if x % 10 == 0 {
|
||||
// one trailing 0, divide by 10 and write
|
||||
itoa_fmt(f, x / 10)
|
||||
} else {
|
||||
itoa_fmt(f, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
|
||||
let mut buf = itoa::Buffer::new();
|
||||
wr.write_str(buf.format(value))
|
||||
}
|
||||
}
|
||||
|
||||
mod _naive {
|
||||
use std::fmt;
|
||||
|
||||
pub struct Quality(pub(crate) u16);
|
||||
|
||||
impl fmt::Display for Quality {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
0 => f.write_str("0"),
|
||||
1000 => f.write_str("1"),
|
||||
|
||||
x => {
|
||||
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use http::Version;
|
||||
|
||||
const CODES: &[u16] = &[201, 303, 404, 515];
|
||||
|
||||
fn bench_write_status_line_11(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_status_line v1.1");
|
||||
|
||||
let version = Version::HTTP_11;
|
||||
|
||||
for i in CODES.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_original::write_status_line(version, 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_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_naive::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_write_status_line_10(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_status_line v1.0");
|
||||
|
||||
let version = Version::HTTP_10;
|
||||
|
||||
for i in CODES.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_original::write_status_line(version, 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_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_naive::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_write_status_line_09(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("write_status_line v0.9");
|
||||
|
||||
let version = Version::HTTP_09;
|
||||
|
||||
for i in CODES.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_original::write_status_line(version, 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_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||
b.iter(|| {
|
||||
let mut b = BytesMut::with_capacity(35);
|
||||
_naive::write_status_line(version, i, &mut b);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_write_status_line_11,
|
||||
bench_write_status_line_10,
|
||||
bench_write_status_line_09
|
||||
);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _naive {
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use http::Version;
|
||||
|
||||
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
|
||||
match version {
|
||||
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
|
||||
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
|
||||
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
|
||||
_ => {
|
||||
// other HTTP version handlers do not use this method
|
||||
}
|
||||
}
|
||||
|
||||
bytes.put_slice(n.to_string().as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
mod _new {
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use http::Version;
|
||||
|
||||
const DIGITS_START: u8 = b'0';
|
||||
|
||||
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
|
||||
match version {
|
||||
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
|
||||
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
|
||||
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
|
||||
_ => {
|
||||
// other HTTP version handlers do not use this method
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
bytes.put_u8(b' ');
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
use std::ptr;
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use http::Version;
|
||||
|
||||
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
|
||||
2021222324252627282930313233343536373839\
|
||||
4041424344454647484950515253545556575859\
|
||||
6061626364656667686970717273747576777879\
|
||||
8081828384858687888990919293949596979899";
|
||||
|
||||
pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13;
|
||||
|
||||
pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) {
|
||||
let mut buf: [u8; STATUS_LINE_BUF_SIZE] = *b"HTTP/1.1 ";
|
||||
|
||||
match version {
|
||||
Version::HTTP_2 => buf[5] = b'2',
|
||||
Version::HTTP_10 => buf[7] = b'0',
|
||||
Version::HTTP_09 => {
|
||||
buf[5] = b'0';
|
||||
buf[7] = b'9';
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut curr: isize = 12;
|
||||
let buf_ptr = buf.as_mut_ptr();
|
||||
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
|
||||
let four = n > 999;
|
||||
|
||||
// decode 2 more chars, if > 2 chars
|
||||
let d1 = (n % 100) << 1;
|
||||
n /= 100;
|
||||
curr -= 2;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), 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 as isize), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
}
|
||||
|
||||
bytes.put_slice(&buf);
|
||||
if four {
|
||||
bytes.put_u8(b' ');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
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: &[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: &[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 {
|
||||
#![allow(clippy::uninit_assumed_init)]
|
||||
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
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]);
|
||||
let len = black_box(bts.len());
|
||||
_new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_write_camel_case);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _new {
|
||||
pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
|
||||
// first copy entire (potentially wrong) slice to output
|
||||
let buffer = unsafe {
|
||||
std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len);
|
||||
std::slice::from_raw_parts_mut(buf, len)
|
||||
};
|
||||
|
||||
let mut iter = value.iter();
|
||||
|
||||
// first character should be uppercase
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[0] = c & 0b1101_1111;
|
||||
}
|
||||
|
||||
// track 1 ahead of the current position since that's the location being assigned to
|
||||
let mut index = 2;
|
||||
|
||||
// remaining characters after hyphens should also be uppercase
|
||||
while let Some(&c) = iter.next() {
|
||||
if c == b'-' {
|
||||
// advance iter by one and uppercase if needed
|
||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||
buffer[index] = c & 0b1101_1111;
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _original {
|
||||
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
||||
let mut index = 0;
|
||||
let key = value;
|
||||
let mut key_iter = key.iter();
|
||||
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
while let Some(c) = key_iter.next() {
|
||||
buffer[index] = *c;
|
||||
index += 1;
|
||||
if *c == b'-' {
|
||||
if let Some(c) = key_iter.next() {
|
||||
if *c >= b'a' && *c <= b'z' {
|
||||
buffer[index] = *c ^ b' ';
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
use actix_http::HttpService;
|
||||
use actix_server::Server;
|
||||
use actix_service::map_config;
|
||||
use actix_web::{dev::AppConfig, get, App};
|
||||
|
||||
#[get("/")]
|
||||
async fn index() -> &'static str {
|
||||
"Hello, world. From Actix Web!"
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Server::build()
|
||||
.bind("hello-world", "127.0.0.1:8080", || {
|
||||
// construct actix-web app
|
||||
let app = App::new().service(index);
|
||||
|
||||
HttpService::build()
|
||||
// pass the app to service builder
|
||||
// map_config is used to map App's configuration to ServiceBuilder
|
||||
.finish(map_config(app, |_| AppConfig::default()))
|
||||
.tcp()
|
||||
})?
|
||||
.run()
|
||||
.await
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
use std::{convert::Infallible, io, time::Duration};
|
||||
|
||||
use actix_http::{HttpService, Request, Response, StatusCode};
|
||||
use actix_server::Server;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static STR: Lazy<String> = Lazy::new(|| "HELLO WORLD ".repeat(20));
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
Server::build()
|
||||
.bind("dispatcher-benchmark", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.finish(|_: Request| async move {
|
||||
let mut res = Response::build(StatusCode::OK);
|
||||
Ok::<_, Infallible>(res.body(&**STR))
|
||||
})
|
||||
.tcp()
|
||||
})?
|
||||
// limiting number of workers so that bench client is not sharing as many resources
|
||||
.workers(4)
|
||||
.run()
|
||||
.await
|
||||
}
|
@ -1,38 +1,37 @@
|
||||
use std::{io, time::Duration};
|
||||
use std::{env, io};
|
||||
|
||||
use actix_http::{Error, HttpService, Request, Response, StatusCode};
|
||||
use actix_http::{error::PayloadError, HttpService, Request, Response};
|
||||
use actix_server::Server;
|
||||
use bytes::BytesMut;
|
||||
use futures_util::StreamExt as _;
|
||||
use futures::{Future, Stream};
|
||||
use http::header::HeaderValue;
|
||||
use log::info;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
fn main() -> io::Result<()> {
|
||||
env::set_var("RUST_LOG", "echo=info");
|
||||
env_logger::init();
|
||||
|
||||
Server::build()
|
||||
.bind("echo", ("127.0.0.1", 8080), || {
|
||||
.bind("echo", "127.0.0.1:8080", || {
|
||||
HttpService::build()
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
// handles HTTP/1.1 and HTTP/2
|
||||
.finish(|mut req: Request| async move {
|
||||
let mut body = BytesMut::new();
|
||||
while let Some(item) = req.payload().next().await {
|
||||
body.extend_from_slice(&item?);
|
||||
}
|
||||
|
||||
log::info!("request body: {:?}", body);
|
||||
|
||||
let res = Response::build(StatusCode::OK)
|
||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||
.body(body);
|
||||
|
||||
Ok::<_, Error>(res)
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.finish(|mut req: Request| {
|
||||
req.take_payload()
|
||||
.fold(BytesMut::new(), move |mut body, chunk| {
|
||||
body.extend_from_slice(&chunk);
|
||||
Ok::<_, PayloadError>(body)
|
||||
})
|
||||
.and_then(|bytes| {
|
||||
info!("request body: {:?}", bytes);
|
||||
let mut res = Response::Ok();
|
||||
res.header(
|
||||
"x-head",
|
||||
HeaderValue::from_static("dummy value!"),
|
||||
);
|
||||
Ok(res.body(bytes))
|
||||
})
|
||||
})
|
||||
// No TLS
|
||||
.tcp()
|
||||
})?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
@ -1,35 +1,34 @@
|
||||
use std::io;
|
||||
use std::{env, io};
|
||||
|
||||
use actix_http::{
|
||||
body::{BodyStream, MessageBody},
|
||||
header, Error, HttpMessage, HttpService, Request, Response, StatusCode,
|
||||
};
|
||||
use actix_http::http::HeaderValue;
|
||||
use actix_http::{error::PayloadError, Error, HttpService, Request, Response};
|
||||
use actix_server::Server;
|
||||
use bytes::BytesMut;
|
||||
use futures::{Future, Stream};
|
||||
use log::info;
|
||||
|
||||
async fn handle_request(mut req: Request) -> Result<Response<impl MessageBody>, Error> {
|
||||
let mut res = Response::build(StatusCode::OK);
|
||||
|
||||
if let Some(ct) = req.headers().get(header::CONTENT_TYPE) {
|
||||
res.insert_header((header::CONTENT_TYPE, ct));
|
||||
}
|
||||
|
||||
// echo request payload stream as (chunked) response body
|
||||
let res = res.message_body(BodyStream::new(req.payload().take()))?;
|
||||
|
||||
Ok(res)
|
||||
fn handle_request(mut req: Request) -> impl Future<Item = Response, Error = Error> {
|
||||
req.take_payload()
|
||||
.fold(BytesMut::new(), move |mut body, chunk| {
|
||||
body.extend_from_slice(&chunk);
|
||||
Ok::<_, PayloadError>(body)
|
||||
})
|
||||
.from_err()
|
||||
.and_then(|bytes| {
|
||||
info!("request body: {:?}", bytes);
|
||||
let mut res = Response::Ok();
|
||||
res.header("x-head", HeaderValue::from_static("dummy value!"));
|
||||
Ok(res.body(bytes))
|
||||
})
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
fn main() -> io::Result<()> {
|
||||
env::set_var("RUST_LOG", "echo=info");
|
||||
env_logger::init();
|
||||
|
||||
actix_server::Server::build()
|
||||
.bind("echo", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
// handles HTTP/1.1 only
|
||||
.h1(handle_request)
|
||||
// No TLS
|
||||
.tcp()
|
||||
Server::build()
|
||||
.bind("echo", "127.0.0.1:8080", || {
|
||||
HttpService::build().finish(|_req: Request| handle_request(_req))
|
||||
})?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
use std::{convert::Infallible, io};
|
||||
|
||||
use actix_http::{HttpService, Request, Response, StatusCode};
|
||||
use actix_server::Server;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static STR: Lazy<String> = Lazy::new(|| "HELLO WORLD ".repeat(100));
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
Server::build()
|
||||
.bind("h2spec", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.h2(|_: Request| async move {
|
||||
let mut res = Response::build(StatusCode::OK);
|
||||
Ok::<_, Infallible>(res.body(&**STR))
|
||||
})
|
||||
.tcp()
|
||||
})?
|
||||
.workers(4)
|
||||
.run()
|
||||
.await
|
||||
}
|
@ -1,38 +1,26 @@
|
||||
use std::{convert::Infallible, io, time::Duration};
|
||||
use std::{env, io};
|
||||
|
||||
use actix_http::{
|
||||
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
|
||||
};
|
||||
use actix_http::{HttpService, Response};
|
||||
use actix_server::Server;
|
||||
use futures::future;
|
||||
use http::header::HeaderValue;
|
||||
use log::info;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
fn main() -> io::Result<()> {
|
||||
env::set_var("RUST_LOG", "hello_world=info");
|
||||
env_logger::init();
|
||||
|
||||
Server::build()
|
||||
.bind("hello-world", ("127.0.0.1", 8080), || {
|
||||
.bind("hello-world", "127.0.0.1:8080", || {
|
||||
HttpService::build()
|
||||
.client_request_timeout(Duration::from_secs(1))
|
||||
.client_disconnect_timeout(Duration::from_secs(1))
|
||||
.on_connect_ext(|_, ext| {
|
||||
ext.insert(42u32);
|
||||
.client_timeout(1000)
|
||||
.client_disconnect(1000)
|
||||
.finish(|_req| {
|
||||
info!("{:?}", _req);
|
||||
let mut res = Response::Ok();
|
||||
res.header("x-head", HeaderValue::from_static("dummy value!"));
|
||||
future::ok::<_, ()>(res.body("Hello world!"))
|
||||
})
|
||||
.finish(|req: Request| async move {
|
||||
log::info!("{:?}", req);
|
||||
|
||||
let mut res = Response::build(StatusCode::OK);
|
||||
res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
|
||||
|
||||
let forty_two = req.extensions().get::<u32>().unwrap().to_string();
|
||||
res.insert_header((
|
||||
"x-forty-two",
|
||||
HeaderValue::from_str(&forty_two).unwrap(),
|
||||
));
|
||||
|
||||
Ok::<_, Infallible>(res.body("Hello world!"))
|
||||
})
|
||||
.tcp()
|
||||
})?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
//! Example showing response body (chunked) stream erroring.
|
||||
//!
|
||||
//! Test using `nc` or `curl`.
|
||||
//! ```sh
|
||||
//! $ curl -vN 127.0.0.1:8080
|
||||
//! $ echo 'GET / HTTP/1.1\n\n' | nc 127.0.0.1 8080
|
||||
//! ```
|
||||
|
||||
use std::{convert::Infallible, io, time::Duration};
|
||||
|
||||
use actix_http::{body::BodyStream, HttpService, Response};
|
||||
use actix_server::Server;
|
||||
use async_stream::stream;
|
||||
use bytes::Bytes;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
Server::build()
|
||||
.bind("streaming-error", ("127.0.0.1", 8080), || {
|
||||
HttpService::build()
|
||||
.finish(|req| async move {
|
||||
log::info!("{:?}", req);
|
||||
let res = Response::ok();
|
||||
|
||||
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
|
||||
yield Ok(Bytes::from("123"));
|
||||
yield Ok(Bytes::from("456"));
|
||||
|
||||
actix_rt::time::sleep(Duration::from_millis(1000)).await;
|
||||
|
||||
yield Err(io::Error::new(io::ErrorKind::Other, ""));
|
||||
})))
|
||||
})
|
||||
.tcp()
|
||||
})?
|
||||
.run()
|
||||
.await
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
//! Sets up a WebSocket server over TCP and TLS.
|
||||
//! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames.
|
||||
|
||||
extern crate tls_rustls as rustls;
|
||||
|
||||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use actix_codec::Encoder;
|
||||
use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
|
||||
use actix_rt::time::{interval, Interval};
|
||||
use actix_server::Server;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
use futures_core::{ready, Stream};
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
Server::build()
|
||||
.bind("tcp", ("127.0.0.1", 8080), || {
|
||||
HttpService::build().h1(handler).tcp()
|
||||
})?
|
||||
.bind("tls", ("127.0.0.1", 8443), || {
|
||||
HttpService::build().finish(handler).rustls(tls_config())
|
||||
})?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
|
||||
log::info!("handshaking");
|
||||
let mut res = ws::handshake(req.head())?;
|
||||
|
||||
// handshake will always fail under HTTP/2
|
||||
|
||||
log::info!("responding");
|
||||
Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?)
|
||||
}
|
||||
|
||||
struct Heartbeat {
|
||||
codec: ws::Codec,
|
||||
interval: Interval,
|
||||
}
|
||||
|
||||
impl Heartbeat {
|
||||
fn new(codec: ws::Codec) -> Self {
|
||||
Self {
|
||||
codec,
|
||||
interval: interval(Duration::from_secs(4)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Heartbeat {
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
log::trace!("poll");
|
||||
|
||||
ready!(self.as_mut().interval.poll_tick(cx));
|
||||
|
||||
let mut buffer = BytesMut::new();
|
||||
|
||||
self.as_mut()
|
||||
.codec
|
||||
.encode(
|
||||
ws::Message::Text(ByteString::from_static("hello world")),
|
||||
&mut buffer,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Poll::Ready(Some(Ok(buffer.freeze())))
|
||||
}
|
||||
}
|
||||
|
||||
fn tls_config() -> rustls::ServerConfig {
|
||||
use std::io::BufReader;
|
||||
|
||||
use rustls::{Certificate, PrivateKey};
|
||||
use rustls_pemfile::{certs, pkcs8_private_keys};
|
||||
|
||||
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
|
||||
let cert_file = cert.serialize_pem().unwrap();
|
||||
let key_file = cert.serialize_private_key_pem();
|
||||
|
||||
let cert_file = &mut BufReader::new(cert_file.as_bytes());
|
||||
let key_file = &mut BufReader::new(key_file.as_bytes());
|
||||
|
||||
let cert_chain = certs(cert_file)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(Certificate)
|
||||
.collect();
|
||||
let mut keys = pkcs8_private_keys(key_file).unwrap();
|
||||
|
||||
let mut config = rustls::ServerConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
|
||||
.unwrap();
|
||||
|
||||
config.alpn_protocols.push(b"http/1.1".to_vec());
|
||||
config.alpn_protocols.push(b"h2".to_vec());
|
||||
|
||||
config
|
||||
}
|
5
actix-http/rustfmt.toml
Normal file
5
actix-http/rustfmt.toml
Normal file
@ -0,0 +1,5 @@
|
||||
max_width = 89
|
||||
reorder_imports = true
|
||||
#wrap_comments = true
|
||||
#fn_args_density = "Compressed"
|
||||
#use_small_heuristics = false
|
570
actix-http/src/body.rs
Normal file
570
actix-http/src/body.rs
Normal file
@ -0,0 +1,570 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::{fmt, mem};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{Async, Poll, Stream};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
/// Body size hint
|
||||
pub enum BodySize {
|
||||
None,
|
||||
Empty,
|
||||
Sized(usize),
|
||||
Sized64(u64),
|
||||
Stream,
|
||||
}
|
||||
|
||||
impl BodySize {
|
||||
pub fn is_eof(&self) -> bool {
|
||||
match self {
|
||||
BodySize::None
|
||||
| BodySize::Empty
|
||||
| BodySize::Sized(0)
|
||||
| BodySize::Sized64(0) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type that provides this trait can be streamed to a peer.
|
||||
pub trait MessageBody {
|
||||
fn size(&self) -> BodySize;
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error>;
|
||||
}
|
||||
|
||||
impl MessageBody for () {
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Empty
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
Ok(Async::Ready(None))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: MessageBody> MessageBody for Box<T> {
|
||||
fn size(&self) -> BodySize {
|
||||
self.as_ref().size()
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
self.as_mut().poll_next()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ResponseBody<B> {
|
||||
Body(B),
|
||||
Other(Body),
|
||||
}
|
||||
|
||||
impl ResponseBody<Body> {
|
||||
pub fn into_body<B>(self) -> ResponseBody<B> {
|
||||
match self {
|
||||
ResponseBody::Body(b) => ResponseBody::Other(b),
|
||||
ResponseBody::Other(b) => ResponseBody::Other(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> ResponseBody<B> {
|
||||
pub fn take_body(&mut self) -> ResponseBody<B> {
|
||||
std::mem::replace(self, ResponseBody::Other(Body::None))
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: MessageBody> ResponseBody<B> {
|
||||
pub fn as_ref(&self) -> Option<&B> {
|
||||
if let ResponseBody::Body(ref b) = self {
|
||||
Some(b)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: MessageBody> MessageBody for ResponseBody<B> {
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
ResponseBody::Body(ref body) => body.size(),
|
||||
ResponseBody::Other(ref body) => body.size(),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
match self {
|
||||
ResponseBody::Body(ref mut body) => body.poll_next(),
|
||||
ResponseBody::Other(ref mut body) => body.poll_next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: MessageBody> Stream for ResponseBody<B> {
|
||||
type Item = Bytes;
|
||||
type Error = Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||
self.poll_next()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents various types of http message body.
|
||||
pub enum Body {
|
||||
/// Empty response. `Content-Length` header is not set.
|
||||
None,
|
||||
/// Zero sized response body. `Content-Length` header is set to `0`.
|
||||
Empty,
|
||||
/// Specific response body.
|
||||
Bytes(Bytes),
|
||||
/// Generic message body.
|
||||
Message(Box<dyn MessageBody>),
|
||||
}
|
||||
|
||||
impl Body {
|
||||
/// Create body from slice (copy)
|
||||
pub fn from_slice(s: &[u8]) -> Body {
|
||||
Body::Bytes(Bytes::from(s))
|
||||
}
|
||||
|
||||
/// Create body from generic message body.
|
||||
pub fn from_message<B: MessageBody + 'static>(body: B) -> Body {
|
||||
Body::Message(Box::new(body))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Body {
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
Body::None => BodySize::None,
|
||||
Body::Empty => BodySize::Empty,
|
||||
Body::Bytes(ref bin) => BodySize::Sized(bin.len()),
|
||||
Body::Message(ref body) => body.size(),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
match self {
|
||||
Body::None => Ok(Async::Ready(None)),
|
||||
Body::Empty => Ok(Async::Ready(None)),
|
||||
Body::Bytes(ref mut bin) => {
|
||||
let len = bin.len();
|
||||
if len == 0 {
|
||||
Ok(Async::Ready(None))
|
||||
} else {
|
||||
Ok(Async::Ready(Some(mem::replace(bin, Bytes::new()))))
|
||||
}
|
||||
}
|
||||
Body::Message(ref mut body) => body.poll_next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::Bytes(ref b) => match *other {
|
||||
Body::Bytes(ref b2) => b == b2,
|
||||
_ => false,
|
||||
},
|
||||
Body::Message(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Body {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Body::None => write!(f, "Body::None"),
|
||||
Body::Empty => write!(f, "Body::Empty"),
|
||||
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b),
|
||||
Body::Message(_) => write!(f, "Body::Message(_)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Body {
|
||||
fn from(s: &'static str) -> Body {
|
||||
Body::Bytes(Bytes::from_static(s.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for Body {
|
||||
fn from(s: &'static [u8]) -> Body {
|
||||
Body::Bytes(Bytes::from_static(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Body {
|
||||
fn from(vec: Vec<u8>) -> Body {
|
||||
Body::Bytes(Bytes::from(vec))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Body {
|
||||
fn from(s: String) -> Body {
|
||||
s.into_bytes().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a String> for Body {
|
||||
fn from(s: &'a String) -> Body {
|
||||
Body::Bytes(Bytes::from(AsRef::<[u8]>::as_ref(&s)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for Body {
|
||||
fn from(s: Bytes) -> Body {
|
||||
Body::Bytes(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BytesMut> for Body {
|
||||
fn from(s: BytesMut) -> Body {
|
||||
Body::Bytes(s.freeze())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Value> for Body {
|
||||
fn from(v: serde_json::Value) -> Body {
|
||||
Body::Bytes(v.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<SizedStream<S>> for Body
|
||||
where
|
||||
S: Stream<Item = Bytes, Error = Error> + 'static,
|
||||
{
|
||||
fn from(s: SizedStream<S>) -> Body {
|
||||
Body::from_message(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> From<BodyStream<S, E>> for Body
|
||||
where
|
||||
S: Stream<Item = Bytes, Error = E> + 'static,
|
||||
E: Into<Error> + 'static,
|
||||
{
|
||||
fn from(s: BodyStream<S, E>) -> Body {
|
||||
Body::from_message(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Bytes {
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len())
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
if self.is_empty() {
|
||||
Ok(Async::Ready(None))
|
||||
} else {
|
||||
Ok(Async::Ready(Some(mem::replace(self, Bytes::new()))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for BytesMut {
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len())
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
if self.is_empty() {
|
||||
Ok(Async::Ready(None))
|
||||
} else {
|
||||
Ok(Async::Ready(Some(
|
||||
mem::replace(self, BytesMut::new()).freeze(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static str {
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len())
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
if self.is_empty() {
|
||||
Ok(Async::Ready(None))
|
||||
} else {
|
||||
Ok(Async::Ready(Some(Bytes::from_static(
|
||||
mem::replace(self, "").as_ref(),
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static [u8] {
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len())
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
if self.is_empty() {
|
||||
Ok(Async::Ready(None))
|
||||
} else {
|
||||
Ok(Async::Ready(Some(Bytes::from_static(mem::replace(
|
||||
self, b"",
|
||||
)))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Vec<u8> {
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len())
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
if self.is_empty() {
|
||||
Ok(Async::Ready(None))
|
||||
} else {
|
||||
Ok(Async::Ready(Some(Bytes::from(mem::replace(
|
||||
self,
|
||||
Vec::new(),
|
||||
)))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for String {
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len())
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
if self.is_empty() {
|
||||
Ok(Async::Ready(None))
|
||||
} else {
|
||||
Ok(Async::Ready(Some(Bytes::from(
|
||||
mem::replace(self, String::new()).into_bytes(),
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type represent streaming body.
|
||||
/// Response does not contain `content-length` header and appropriate transfer encoding is used.
|
||||
pub struct BodyStream<S, E> {
|
||||
stream: S,
|
||||
_t: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<S, E> BodyStream<S, E>
|
||||
where
|
||||
S: Stream<Item = Bytes, Error = E>,
|
||||
E: Into<Error>,
|
||||
{
|
||||
pub fn new(stream: S) -> Self {
|
||||
BodyStream {
|
||||
stream,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> MessageBody for BodyStream<S, E>
|
||||
where
|
||||
S: Stream<Item = Bytes, Error = E>,
|
||||
E: Into<Error>,
|
||||
{
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Stream
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
self.stream.poll().map_err(std::convert::Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type represent streaming body. This body implementation should be used
|
||||
/// if total size of stream is known. Data get sent as is without using transfer encoding.
|
||||
pub struct SizedStream<S> {
|
||||
size: u64,
|
||||
stream: S,
|
||||
}
|
||||
|
||||
impl<S> SizedStream<S>
|
||||
where
|
||||
S: Stream<Item = Bytes, Error = Error>,
|
||||
{
|
||||
pub fn new(size: u64, stream: S) -> Self {
|
||||
SizedStream { size, stream }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> MessageBody for SizedStream<S>
|
||||
where
|
||||
S: Stream<Item = Bytes, Error = Error>,
|
||||
{
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized64(self.size)
|
||||
}
|
||||
|
||||
fn poll_next(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
self.stream.poll()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
impl Body {
|
||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
||||
match *self {
|
||||
Body::Bytes(ref bin) => &bin,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseBody<Body> {
|
||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
||||
match *self {
|
||||
ResponseBody::Body(ref b) => b.get_ref(),
|
||||
ResponseBody::Other(ref b) => b.get_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_str() {
|
||||
assert_eq!(Body::from("").size(), BodySize::Sized(0));
|
||||
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from("test").get_ref(), b"test");
|
||||
|
||||
assert_eq!("test".size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
"test".poll_next().unwrap(),
|
||||
Async::Ready(Some(Bytes::from("test")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_bytes() {
|
||||
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
|
||||
assert_eq!(
|
||||
Body::from_slice(b"test".as_ref()).size(),
|
||||
BodySize::Sized(4)
|
||||
);
|
||||
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
|
||||
|
||||
assert_eq!((&b"test"[..]).size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
(&b"test"[..]).poll_next().unwrap(),
|
||||
Async::Ready(Some(Bytes::from("test")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vec() {
|
||||
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
|
||||
|
||||
assert_eq!(Vec::from("test").size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
Vec::from("test").poll_next().unwrap(),
|
||||
Async::Ready(Some(Bytes::from("test")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bytes() {
|
||||
let mut b = Bytes::from("test");
|
||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
||||
|
||||
assert_eq!(b.size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
b.poll_next().unwrap(),
|
||||
Async::Ready(Some(Bytes::from("test")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bytes_mut() {
|
||||
let mut b = BytesMut::from("test");
|
||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
||||
|
||||
assert_eq!(b.size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
b.poll_next().unwrap(),
|
||||
Async::Ready(Some(Bytes::from("test")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string() {
|
||||
let mut b = "test".to_owned();
|
||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
||||
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(&b).get_ref(), b"test");
|
||||
|
||||
assert_eq!(b.size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
b.poll_next().unwrap(),
|
||||
Async::Ready(Some(Bytes::from("test")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unit() {
|
||||
assert_eq!(().size(), BodySize::Empty);
|
||||
assert_eq!(().poll_next().unwrap(), Async::Ready(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_box() {
|
||||
let mut val = Box::new(());
|
||||
assert_eq!(val.size(), BodySize::Empty);
|
||||
assert_eq!(val.poll_next().unwrap(), Async::Ready(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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"))
|
||||
);
|
||||
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serde_json() {
|
||||
use serde_json::json;
|
||||
assert_eq!(
|
||||
Body::from(serde_json::Value::String("test".into())).size(),
|
||||
BodySize::Sized(6)
|
||||
);
|
||||
assert_eq!(
|
||||
Body::from(json!({"test-key":"test-value"})).size(),
|
||||
BodySize::Sized(25)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,215 +0,0 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Stream};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use super::{BodySize, MessageBody};
|
||||
|
||||
pin_project! {
|
||||
/// Streaming response wrapper.
|
||||
///
|
||||
/// Response does not contain `Content-Length` header and appropriate transfer encoding is used.
|
||||
pub struct BodyStream<S> {
|
||||
#[pin]
|
||||
stream: S,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: from_infallible method
|
||||
|
||||
impl<S, E> BodyStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>>,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
#[inline]
|
||||
pub fn new(stream: S) -> Self {
|
||||
BodyStream { stream }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> MessageBody for BodyStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>>,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Stream
|
||||
}
|
||||
|
||||
/// Attempts to pull out the next value of the underlying [`Stream`].
|
||||
///
|
||||
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
|
||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||
/// [`Stream`] ends.
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
loop {
|
||||
let stream = self.as_mut().project().stream;
|
||||
|
||||
let chunk = match ready!(stream.poll_next(cx)) {
|
||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||
opt => opt,
|
||||
};
|
||||
|
||||
return Poll::Ready(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{convert::Infallible, time::Duration};
|
||||
|
||||
use actix_rt::{
|
||||
pin,
|
||||
time::{sleep, Sleep},
|
||||
};
|
||||
use actix_utils::future::poll_fn;
|
||||
use derive_more::{Display, Error};
|
||||
use futures_core::ready;
|
||||
use futures_util::{stream, FutureExt as _};
|
||||
use pin_project_lite::pin_project;
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||
|
||||
use super::*;
|
||||
use crate::body::to_bytes;
|
||||
|
||||
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
|
||||
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
|
||||
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
|
||||
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
|
||||
|
||||
assert_not_impl_any!(BodyStream<stream::Empty<Bytes>>: MessageBody);
|
||||
assert_not_impl_any!(BodyStream<stream::Repeat<Bytes>>: MessageBody);
|
||||
// crate::Error is not Clone
|
||||
assert_not_impl_any!(BodyStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn skips_empty_chunks() {
|
||||
let body = BodyStream::new(stream::iter(
|
||||
["1", "", "2"]
|
||||
.iter()
|
||||
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
|
||||
));
|
||||
pin!(body);
|
||||
|
||||
assert_eq!(
|
||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.ok(),
|
||||
Some(Bytes::from("1")),
|
||||
);
|
||||
assert_eq!(
|
||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.ok(),
|
||||
Some(Bytes::from("2")),
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn read_to_bytes() {
|
||||
let body = BodyStream::new(stream::iter(
|
||||
["1", "", "2"]
|
||||
.iter()
|
||||
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
|
||||
));
|
||||
|
||||
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
|
||||
}
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(fmt = "stream error")]
|
||||
struct StreamErr;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_immediate_error() {
|
||||
let body = BodyStream::new(stream::once(async { Err(StreamErr) }));
|
||||
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_string_error() {
|
||||
// `&'static str` does not impl `Error`
|
||||
// but it does impl `Into<Box<dyn Error>>`
|
||||
|
||||
let body = BodyStream::new(stream::once(async { Err("stringy error") }));
|
||||
assert!(matches!(to_bytes(body).await, Err("stringy error")));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_boxed_error() {
|
||||
// `Box<dyn Error>` does not impl `Error`
|
||||
// but it does impl `Into<Box<dyn Error>>`
|
||||
|
||||
let body = BodyStream::new(stream::once(async {
|
||||
Err(Box::<dyn StdError>::from("stringy error"))
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
to_bytes(body).await.unwrap_err().to_string(),
|
||||
"stringy error"
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_delayed_error() {
|
||||
let body = BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
|
||||
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||
|
||||
pin_project! {
|
||||
#[derive(Debug)]
|
||||
#[project = TimeDelayStreamProj]
|
||||
enum TimeDelayStream {
|
||||
Start,
|
||||
Sleep { delay: Pin<Box<Sleep>> },
|
||||
Done,
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for TimeDelayStream {
|
||||
type Item = Result<Bytes, StreamErr>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
match self.as_mut().get_mut() {
|
||||
TimeDelayStream::Start => {
|
||||
let sleep = sleep(Duration::from_millis(1));
|
||||
self.as_mut().set(TimeDelayStream::Sleep {
|
||||
delay: Box::pin(sleep),
|
||||
});
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
TimeDelayStream::Sleep { ref mut delay } => {
|
||||
ready!(delay.poll_unpin(cx));
|
||||
self.set(TimeDelayStream::Done);
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
TimeDelayStream::Done => Poll::Ready(Some(Err(StreamErr))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let body = BodyStream::new(TimeDelayStream::Start);
|
||||
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||
}
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::{BodySize, MessageBody, MessageBodyMapErr};
|
||||
use crate::body;
|
||||
|
||||
/// A boxed message body with boxed errors.
|
||||
#[derive(Debug)]
|
||||
pub struct BoxBody(BoxBodyInner);
|
||||
|
||||
enum BoxBodyInner {
|
||||
None(body::None),
|
||||
Bytes(Bytes),
|
||||
Stream(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for BoxBodyInner {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::None(arg0) => f.debug_tuple("None").field(arg0).finish(),
|
||||
Self::Bytes(arg0) => f.debug_tuple("Bytes").field(arg0).finish(),
|
||||
Self::Stream(_) => f.debug_tuple("Stream").field(&"dyn MessageBody").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxBody {
|
||||
/// Boxes body type, erasing type information.
|
||||
///
|
||||
/// If the body type to wrap is unknown or generic it is better to use [`MessageBody::boxed`] to
|
||||
/// avoid double boxing.
|
||||
#[inline]
|
||||
pub fn new<B>(body: B) -> Self
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
match body.size() {
|
||||
BodySize::None => Self(BoxBodyInner::None(body::None)),
|
||||
_ => match body.try_into_bytes() {
|
||||
Ok(bytes) => Self(BoxBodyInner::Bytes(bytes)),
|
||||
Err(body) => {
|
||||
let body = MessageBodyMapErr::new(body, Into::into);
|
||||
Self(BoxBodyInner::Stream(Box::pin(body)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a mutable pinned reference to the inner message body type.
|
||||
#[inline]
|
||||
pub fn as_pin_mut(&mut self) -> Pin<&mut Self> {
|
||||
Pin::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for BoxBody {
|
||||
type Error = Box<dyn StdError>;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
match &self.0 {
|
||||
BoxBodyInner::None(none) => none.size(),
|
||||
BoxBodyInner::Bytes(bytes) => bytes.size(),
|
||||
BoxBodyInner::Stream(stream) => stream.size(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match &mut self.0 {
|
||||
BoxBodyInner::None(body) => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
BoxBodyInner::Bytes(body) => {
|
||||
Pin::new(body).poll_next(cx).map_err(|err| match err {})
|
||||
}
|
||||
BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
match self.0 {
|
||||
BoxBodyInner::None(body) => Ok(body.try_into_bytes().unwrap()),
|
||||
BoxBodyInner::Bytes(body) => Ok(body.try_into_bytes().unwrap()),
|
||||
_ => Err(self),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||
|
||||
use super::*;
|
||||
use crate::body::to_bytes;
|
||||
|
||||
assert_impl_all!(BoxBody: fmt::Debug, MessageBody, Unpin);
|
||||
assert_not_impl_any!(BoxBody: Send, Sync);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn nested_boxed_body() {
|
||||
let body = Bytes::from_static(&[1, 2, 3]);
|
||||
let boxed_body = BoxBody::new(BoxBody::new(body));
|
||||
|
||||
assert_eq!(
|
||||
to_bytes(boxed_body).await.unwrap(),
|
||||
Bytes::from(vec![1, 2, 3]),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use super::{BodySize, BoxBody, MessageBody};
|
||||
use crate::Error;
|
||||
|
||||
pin_project! {
|
||||
/// An "either" type specialized for body types.
|
||||
///
|
||||
/// It is common, in middleware especially, to conditionally return an inner service's unknown/
|
||||
/// generic body `B` type or return early with a new response. This type's "right" variant
|
||||
/// defaults to `BoxBody` since error responses are the common case.
|
||||
///
|
||||
/// For example, middleware will often have `type Response = ServiceResponse<EitherBody<B>>`.
|
||||
/// This means that the inner service's response body type maps to the `Left` variant and the
|
||||
/// middleware's own error responses use the default `Right` variant of `BoxBody`. Of course,
|
||||
/// there's no reason it couldn't use `EitherBody<B, String>` instead if its alternative
|
||||
/// responses have a known type.
|
||||
#[project = EitherBodyProj]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EitherBody<L, R = BoxBody> {
|
||||
/// A body of type `L`.
|
||||
Left { #[pin] body: L },
|
||||
|
||||
/// A body of type `R`.
|
||||
Right { #[pin] body: R },
|
||||
}
|
||||
}
|
||||
|
||||
impl<L> EitherBody<L, BoxBody> {
|
||||
/// Creates new `EitherBody` left variant with a boxed right variant.
|
||||
///
|
||||
/// If the expected `R` type will be inferred and is not `BoxBody` then use the
|
||||
/// [`left`](Self::left) constructor instead.
|
||||
#[inline]
|
||||
pub fn new(body: L) -> Self {
|
||||
Self::Left { body }
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, R> EitherBody<L, R> {
|
||||
/// Creates new `EitherBody` using left variant.
|
||||
#[inline]
|
||||
pub fn left(body: L) -> Self {
|
||||
Self::Left { body }
|
||||
}
|
||||
|
||||
/// Creates new `EitherBody` using right variant.
|
||||
#[inline]
|
||||
pub fn right(body: R) -> Self {
|
||||
Self::Right { body }
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, R> MessageBody for EitherBody<L, R>
|
||||
where
|
||||
L: MessageBody + 'static,
|
||||
R: MessageBody + 'static,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
EitherBody::Left { body } => body.size(),
|
||||
EitherBody::Right { body } => body.size(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match self.project() {
|
||||
EitherBodyProj::Left { body } => body
|
||||
.poll_next(cx)
|
||||
.map_err(|err| Error::new_body().with_cause(err)),
|
||||
EitherBodyProj::Right { body } => body
|
||||
.poll_next(cx)
|
||||
.map_err(|err| Error::new_body().with_cause(err)),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
match self {
|
||||
EitherBody::Left { body } => body
|
||||
.try_into_bytes()
|
||||
.map_err(|body| EitherBody::Left { body }),
|
||||
EitherBody::Right { body } => body
|
||||
.try_into_bytes()
|
||||
.map_err(|body| EitherBody::Right { body }),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody {
|
||||
match self {
|
||||
EitherBody::Left { body } => body.boxed(),
|
||||
EitherBody::Right { body } => body.boxed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn type_parameter_inference() {
|
||||
let _body: EitherBody<(), _> = EitherBody::new(());
|
||||
|
||||
let _body: EitherBody<_, ()> = EitherBody::left(());
|
||||
let _body: EitherBody<(), _> = EitherBody::right(());
|
||||
}
|
||||
}
|
@ -1,609 +0,0 @@
|
||||
//! [`MessageBody`] trait and foreign implementations.
|
||||
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
error::Error as StdError,
|
||||
mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::ready;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use super::{BodySize, BoxBody};
|
||||
|
||||
/// An interface for types that can be used as a response body.
|
||||
///
|
||||
/// It is not usually necessary to create custom body types, this trait is already [implemented for
|
||||
/// a large number of sensible body types](#foreign-impls) including:
|
||||
/// - Empty body: `()`
|
||||
/// - Text-based: `String`, `&'static str`, [`ByteString`](https://docs.rs/bytestring/1).
|
||||
/// - Byte-based: `Bytes`, `BytesMut`, `Vec<u8>`, `&'static [u8]`;
|
||||
/// - Streams: [`BodyStream`](super::BodyStream), [`SizedStream`](super::SizedStream)
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::convert::Infallible;
|
||||
/// # use std::task::{Poll, Context};
|
||||
/// # use std::pin::Pin;
|
||||
/// # use bytes::Bytes;
|
||||
/// # use actix_http::body::{BodySize, MessageBody};
|
||||
/// struct Repeat {
|
||||
/// chunk: String,
|
||||
/// n_times: usize,
|
||||
/// }
|
||||
///
|
||||
/// impl MessageBody for Repeat {
|
||||
/// type Error = Infallible;
|
||||
///
|
||||
/// fn size(&self) -> BodySize {
|
||||
/// BodySize::Sized((self.chunk.len() * self.n_times) as u64)
|
||||
/// }
|
||||
///
|
||||
/// fn poll_next(
|
||||
/// self: Pin<&mut Self>,
|
||||
/// _cx: &mut Context<'_>,
|
||||
/// ) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
/// let payload_string = self.chunk.repeat(self.n_times);
|
||||
/// let payload_bytes = Bytes::from(payload_string);
|
||||
/// Poll::Ready(Some(Ok(payload_bytes)))
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait MessageBody {
|
||||
/// The type of error that will be returned if streaming body fails.
|
||||
///
|
||||
/// Since it is not appropriate to generate a response mid-stream, it only requires `Error` for
|
||||
/// internal use and logging.
|
||||
type Error: Into<Box<dyn StdError>>;
|
||||
|
||||
/// Body size hint.
|
||||
///
|
||||
/// If [`BodySize::None`] is returned, optimizations that skip reading the body are allowed.
|
||||
fn size(&self) -> BodySize;
|
||||
|
||||
/// Attempt to pull out the next chunk of body bytes.
|
||||
///
|
||||
/// # Return Value
|
||||
/// Similar to the `Stream` interface, there are several possible return values, each indicating
|
||||
/// a distinct state:
|
||||
/// - `Poll::Pending` means that this body's next chunk is not ready yet. Implementations must
|
||||
/// ensure that the current task will be notified when the next chunk may be ready.
|
||||
/// - `Poll::Ready(Some(val))` means that the body has successfully produced a chunk, `val`,
|
||||
/// and may produce further values on subsequent `poll_next` calls.
|
||||
/// - `Poll::Ready(None)` means that the body is complete, and `poll_next` should not be
|
||||
/// invoked again.
|
||||
///
|
||||
/// # Panics
|
||||
/// Once a body is complete (i.e., `poll_next` returned `Ready(None)`), calling its `poll_next`
|
||||
/// method again may panic, block forever, or cause other kinds of problems; this trait places
|
||||
/// no requirements on the effects of such a call. However, as the `poll_next` method is not
|
||||
/// marked unsafe, Rust’s usual rules apply: calls must never cause UB, regardless of its state.
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>>;
|
||||
|
||||
/// Try to convert into the complete chunk of body bytes.
|
||||
///
|
||||
/// Override this method if the complete body can be trivially extracted. This is useful for
|
||||
/// optimizations where `poll_next` calls can be avoided.
|
||||
///
|
||||
/// Body types with [`BodySize::None`] are allowed to return empty `Bytes`. Although, if calling
|
||||
/// this method, it is recommended to check `size` first and return early.
|
||||
///
|
||||
/// # Errors
|
||||
/// The default implementation will error and return the original type back to the caller for
|
||||
/// further use.
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Err(self)
|
||||
}
|
||||
|
||||
/// Wraps this body into a `BoxBody`.
|
||||
///
|
||||
/// No-op when called on a `BoxBody`, meaning there is no risk of double boxing when calling
|
||||
/// this on a generic `MessageBody`. Prefer this over [`BoxBody::new`] when a boxed body
|
||||
/// is required.
|
||||
#[inline]
|
||||
fn boxed(self) -> BoxBody
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
{
|
||||
BoxBody::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
mod foreign_impls {
|
||||
use super::*;
|
||||
|
||||
impl MessageBody for Infallible {
|
||||
type Error = Infallible;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
match *self {}
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match *self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for () {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
Poll::Ready(None)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for Box<B>
|
||||
where
|
||||
B: MessageBody + Unpin + ?Sized,
|
||||
{
|
||||
type Error = B::Error;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
self.as_ref().size()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
Pin::new(self.get_mut().as_mut()).poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for Pin<Box<B>>
|
||||
where
|
||||
B: MessageBody + ?Sized,
|
||||
{
|
||||
type Error = B::Error;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
self.as_ref().size()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
self.get_mut().as_mut().poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static [u8] {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(Bytes::from_static(mem::take(self.get_mut())))))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from_static(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Bytes {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for BytesMut {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(self.freeze())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Vec<u8> {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).into())))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static str {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let string = mem::take(self.get_mut());
|
||||
let bytes = Bytes::from_static(string.as_bytes());
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from_static(self.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for String {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let string = mem::take(self.get_mut());
|
||||
Poll::Ready(Some(Ok(Bytes::from(string))))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for bytestring::ByteString {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
let string = mem::take(self.get_mut());
|
||||
Poll::Ready(Some(Ok(string.into_bytes())))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(self.into_bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
pub(crate) struct MessageBodyMapErr<B, F> {
|
||||
#[pin]
|
||||
body: B,
|
||||
mapper: Option<F>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, F, E> MessageBodyMapErr<B, F>
|
||||
where
|
||||
B: MessageBody,
|
||||
F: FnOnce(B::Error) -> E,
|
||||
{
|
||||
pub(crate) fn new(body: B, mapper: F) -> Self {
|
||||
Self {
|
||||
body,
|
||||
mapper: Some(mapper),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, F, E> MessageBody for MessageBodyMapErr<B, F>
|
||||
where
|
||||
B: MessageBody,
|
||||
F: FnOnce(B::Error) -> E,
|
||||
E: Into<Box<dyn StdError>>,
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
self.body.size()
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
let this = self.as_mut().project();
|
||||
|
||||
match ready!(this.body.poll_next(cx)) {
|
||||
Some(Err(err)) => {
|
||||
let f = self.as_mut().project().mapper.take().unwrap();
|
||||
let mapped_err = (f)(err);
|
||||
Poll::Ready(Some(Err(mapped_err)))
|
||||
}
|
||||
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
let Self { body, mapper } = self;
|
||||
body.try_into_bytes().map_err(|body| Self { body, mapper })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
use super::*;
|
||||
use crate::body::{self, EitherBody};
|
||||
|
||||
macro_rules! assert_poll_next {
|
||||
($pin:expr, $exp:expr) => {
|
||||
assert_eq!(
|
||||
poll_fn(|cx| $pin.as_mut().poll_next(cx))
|
||||
.await
|
||||
.unwrap() // unwrap option
|
||||
.unwrap(), // unwrap result
|
||||
$exp
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_poll_next_none {
|
||||
($pin:expr) => {
|
||||
assert!(poll_fn(|cx| $pin.as_mut().poll_next(cx)).await.is_none());
|
||||
};
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn boxing_equivalence() {
|
||||
assert_eq!(().size(), BodySize::Sized(0));
|
||||
assert_eq!(().size(), Box::new(()).size());
|
||||
assert_eq!(().size(), Box::pin(()).size());
|
||||
|
||||
let pl = Box::new(());
|
||||
pin!(pl);
|
||||
assert_poll_next_none!(pl);
|
||||
|
||||
let mut pl = Box::pin(());
|
||||
assert_poll_next_none!(pl);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_unit() {
|
||||
let pl = ();
|
||||
assert_eq!(pl.size(), BodySize::Sized(0));
|
||||
pin!(pl);
|
||||
assert_poll_next_none!(pl);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_str() {
|
||||
assert_eq!("".size(), BodySize::Sized(0));
|
||||
assert_eq!("test".size(), BodySize::Sized(4));
|
||||
|
||||
let pl = "test";
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_bytes() {
|
||||
assert_eq!(b"".as_ref().size(), BodySize::Sized(0));
|
||||
assert_eq!(b"test".as_ref().size(), BodySize::Sized(4));
|
||||
|
||||
let pl = b"test".as_ref();
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_vec() {
|
||||
assert_eq!(vec![0; 0].size(), BodySize::Sized(0));
|
||||
assert_eq!(Vec::from("test").size(), BodySize::Sized(4));
|
||||
|
||||
let pl = Vec::from("test");
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_bytes() {
|
||||
assert_eq!(Bytes::new().size(), BodySize::Sized(0));
|
||||
assert_eq!(Bytes::from_static(b"test").size(), BodySize::Sized(4));
|
||||
|
||||
let pl = Bytes::from_static(b"test");
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_bytes_mut() {
|
||||
assert_eq!(BytesMut::new().size(), BodySize::Sized(0));
|
||||
assert_eq!(BytesMut::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
||||
|
||||
let pl = BytesMut::from("test");
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_string() {
|
||||
assert_eq!(String::new().size(), BodySize::Sized(0));
|
||||
assert_eq!("test".to_owned().size(), BodySize::Sized(4));
|
||||
|
||||
let pl = "test".to_owned();
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn complete_body_combinators() {
|
||||
let body = Bytes::from_static(b"test");
|
||||
let body = BoxBody::new(body);
|
||||
let body = EitherBody::<_, ()>::left(body);
|
||||
let body = EitherBody::<(), _>::right(body);
|
||||
// Do not support try_into_bytes:
|
||||
// let body = Box::new(body);
|
||||
// let body = Box::pin(body);
|
||||
|
||||
assert_eq!(body.try_into_bytes().unwrap(), Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn complete_body_combinators_poll() {
|
||||
let body = Bytes::from_static(b"test");
|
||||
let body = BoxBody::new(body);
|
||||
let body = EitherBody::<_, ()>::left(body);
|
||||
let body = EitherBody::<(), _>::right(body);
|
||||
let mut body = body;
|
||||
|
||||
assert_eq!(body.size(), BodySize::Sized(4));
|
||||
assert_poll_next!(Pin::new(&mut body), Bytes::from("test"));
|
||||
assert_poll_next_none!(Pin::new(&mut body));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn none_body_combinators() {
|
||||
fn none_body() -> BoxBody {
|
||||
let body = body::None;
|
||||
let body = BoxBody::new(body);
|
||||
let body = EitherBody::<_, ()>::left(body);
|
||||
let body = EitherBody::<(), _>::right(body);
|
||||
body.boxed()
|
||||
}
|
||||
|
||||
assert_eq!(none_body().size(), BodySize::None);
|
||||
assert_eq!(none_body().try_into_bytes().unwrap(), Bytes::new());
|
||||
assert_poll_next_none!(Pin::new(&mut none_body()));
|
||||
}
|
||||
|
||||
// down-casting used to be done with a method on MessageBody trait
|
||||
// test is kept to demonstrate equivalence of Any trait
|
||||
#[actix_rt::test]
|
||||
async fn test_body_casting() {
|
||||
let mut body = String::from("hello cast");
|
||||
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
|
||||
let resp_body: &mut dyn std::any::Any = &mut body;
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast");
|
||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||
body.push('!');
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast!");
|
||||
let not_body = resp_body.downcast_ref::<()>();
|
||||
assert!(not_body.is_none());
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
//! Traits and structures to aid consuming and writing HTTP payloads.
|
||||
//!
|
||||
//! "Body" and "payload" are used somewhat interchangeably in this documentation.
|
||||
|
||||
// Though the spec kinda reads like "payload" is the possibly-transfer-encoded part of the message
|
||||
// and the "body" is the intended possibly-decoded version of that.
|
||||
|
||||
mod body_stream;
|
||||
mod boxed;
|
||||
mod either;
|
||||
mod message_body;
|
||||
mod none;
|
||||
mod size;
|
||||
mod sized_stream;
|
||||
mod utils;
|
||||
|
||||
pub use self::body_stream::BodyStream;
|
||||
pub use self::boxed::BoxBody;
|
||||
pub use self::either::EitherBody;
|
||||
pub use self::message_body::MessageBody;
|
||||
pub(crate) use self::message_body::MessageBodyMapErr;
|
||||
pub use self::none::None;
|
||||
pub use self::size::BodySize;
|
||||
pub use self::sized_stream::SizedStream;
|
||||
pub use self::utils::to_bytes;
|
@ -1,51 +0,0 @@
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::{BodySize, MessageBody};
|
||||
|
||||
/// Body type for responses that forbid payloads.
|
||||
///
|
||||
/// This is distinct from an "empty" response which _would_ contain a `Content-Length` header.
|
||||
/// For an "empty" body, use `()` or `Bytes::new()`.
|
||||
///
|
||||
/// For example, the HTTP spec forbids a payload to be sent with a `204 No Content` response.
|
||||
/// In this case, the payload (or lack thereof) is implicit from the status code, so a
|
||||
/// `Content-Length` header is not required.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct None;
|
||||
|
||||
impl None {
|
||||
/// Constructs new "none" body.
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for None {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
Poll::Ready(Option::None)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
||||
Ok(Bytes::new())
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/// Body size hint.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BodySize {
|
||||
/// Implicitly empty body.
|
||||
///
|
||||
/// Will omit the Content-Length header. Used for responses to certain methods (e.g., `HEAD`) or
|
||||
/// with particular status codes (e.g., 204 No Content). Consumers that read this as a body size
|
||||
/// hint are allowed to make optimizations that skip reading or writing the payload.
|
||||
None,
|
||||
|
||||
/// Known size body.
|
||||
///
|
||||
/// Will write `Content-Length: N` header.
|
||||
Sized(u64),
|
||||
|
||||
/// Unknown size body.
|
||||
///
|
||||
/// Will not write Content-Length header. Can be used with chunked Transfer-Encoding.
|
||||
Stream,
|
||||
}
|
||||
|
||||
impl BodySize {
|
||||
/// Equivalent to `BodySize::Sized(0)`;
|
||||
pub const ZERO: Self = Self::Sized(0);
|
||||
|
||||
/// Returns true if size hint indicates omitted or empty body.
|
||||
///
|
||||
/// Streams will return false because it cannot be known without reading the stream.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_http::body::BodySize;
|
||||
/// assert!(BodySize::None.is_eof());
|
||||
/// assert!(BodySize::Sized(0).is_eof());
|
||||
///
|
||||
/// assert!(!BodySize::Sized(64).is_eof());
|
||||
/// assert!(!BodySize::Stream.is_eof());
|
||||
/// ```
|
||||
pub fn is_eof(&self) -> bool {
|
||||
matches!(self, BodySize::None | BodySize::Sized(0))
|
||||
}
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Stream};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use super::{BodySize, MessageBody};
|
||||
|
||||
pin_project! {
|
||||
/// Known sized streaming response wrapper.
|
||||
///
|
||||
/// This body implementation should be used if total size of stream is known. Data is sent as-is
|
||||
/// without using chunked transfer encoding.
|
||||
pub struct SizedStream<S> {
|
||||
size: u64,
|
||||
#[pin]
|
||||
stream: S,
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> SizedStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>>,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
#[inline]
|
||||
pub fn new(size: u64, stream: S) -> Self {
|
||||
SizedStream { size, stream }
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: from_infallible method
|
||||
|
||||
impl<S, E> MessageBody for SizedStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>>,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.size as u64)
|
||||
}
|
||||
|
||||
/// Attempts to pull out the next value of the underlying [`Stream`].
|
||||
///
|
||||
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
|
||||
/// ended on a zero-length chunk, but rather proceed until the underlying
|
||||
/// [`Stream`] ends.
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
loop {
|
||||
let stream = self.as_mut().project().stream;
|
||||
|
||||
let chunk = match ready!(stream.poll_next(cx)) {
|
||||
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
|
||||
val => val,
|
||||
};
|
||||
|
||||
return Poll::Ready(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::Infallible;
|
||||
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use futures_util::stream;
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||
|
||||
use super::*;
|
||||
use crate::body::to_bytes;
|
||||
|
||||
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
|
||||
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
|
||||
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
|
||||
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
|
||||
|
||||
assert_not_impl_any!(SizedStream<stream::Empty<Bytes>>: MessageBody);
|
||||
assert_not_impl_any!(SizedStream<stream::Repeat<Bytes>>: MessageBody);
|
||||
// crate::Error is not Clone
|
||||
assert_not_impl_any!(SizedStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn skips_empty_chunks() {
|
||||
let body = SizedStream::new(
|
||||
2,
|
||||
stream::iter(
|
||||
["1", "", "2"]
|
||||
.iter()
|
||||
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
|
||||
),
|
||||
);
|
||||
|
||||
pin!(body);
|
||||
|
||||
assert_eq!(
|
||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.ok(),
|
||||
Some(Bytes::from("1")),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
poll_fn(|cx| body.as_mut().poll_next(cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.ok(),
|
||||
Some(Bytes::from("2")),
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn read_to_bytes() {
|
||||
let body = SizedStream::new(
|
||||
2,
|
||||
stream::iter(
|
||||
["1", "", "2"]
|
||||
.iter()
|
||||
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_string_error() {
|
||||
// `&'static str` does not impl `Error`
|
||||
// but it does impl `Into<Box<dyn Error>>`
|
||||
|
||||
let body = SizedStream::new(0, stream::once(async { Err("stringy error") }));
|
||||
assert_eq!(to_bytes(body).await, Ok(Bytes::new()));
|
||||
|
||||
let body = SizedStream::new(1, stream::once(async { Err("stringy error") }));
|
||||
assert!(matches!(to_bytes(body).await, Err("stringy error")));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_boxed_error() {
|
||||
// `Box<dyn Error>` does not impl `Error`
|
||||
// but it does impl `Into<Box<dyn Error>>`
|
||||
|
||||
let body = SizedStream::new(
|
||||
0,
|
||||
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
|
||||
);
|
||||
assert_eq!(to_bytes(body).await.unwrap(), Bytes::new());
|
||||
|
||||
let body = SizedStream::new(
|
||||
1,
|
||||
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
|
||||
);
|
||||
assert_eq!(
|
||||
to_bytes(body).await.unwrap_err().to_string(),
|
||||
"stringy error"
|
||||
);
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
use std::task::Poll;
|
||||
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::ready;
|
||||
|
||||
use super::{BodySize, MessageBody};
|
||||
|
||||
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
|
||||
///
|
||||
/// Any errors produced by the body stream are returned immediately.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_http::body::{self, to_bytes};
|
||||
/// use bytes::Bytes;
|
||||
///
|
||||
/// # async fn test_to_bytes() {
|
||||
/// let body = body::None::new();
|
||||
/// let bytes = to_bytes(body).await.unwrap();
|
||||
/// assert!(bytes.is_empty());
|
||||
///
|
||||
/// let body = Bytes::from_static(b"123");
|
||||
/// let bytes = to_bytes(body).await.unwrap();
|
||||
/// assert_eq!(bytes, b"123"[..]);
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
||||
let cap = match body.size() {
|
||||
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
|
||||
BodySize::Sized(size) => size as usize,
|
||||
// good enough first guess for chunk size
|
||||
BodySize::Stream => 32_768,
|
||||
};
|
||||
|
||||
let mut buf = BytesMut::with_capacity(cap);
|
||||
|
||||
pin!(body);
|
||||
|
||||
poll_fn(|cx| loop {
|
||||
let body = body.as_mut();
|
||||
|
||||
match ready!(body.poll_next(cx)) {
|
||||
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||
None => return Poll::Ready(Ok(())),
|
||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(buf.freeze())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use futures_util::{stream, StreamExt as _};
|
||||
|
||||
use super::*;
|
||||
use crate::{body::BodyStream, Error};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_to_bytes() {
|
||||
let bytes = to_bytes(()).await.unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
|
||||
let body = Bytes::from_static(b"123");
|
||||
let bytes = to_bytes(body).await.unwrap();
|
||||
assert_eq!(bytes, b"123"[..]);
|
||||
|
||||
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
|
||||
.map(Ok::<_, Error>);
|
||||
let body = BodyStream::new(stream);
|
||||
let bytes = to_bytes(body).await.unwrap();
|
||||
assert_eq!(bytes, b"123abc"[..]);
|
||||
}
|
||||
}
|
@ -1,129 +1,106 @@
|
||||
use std::{fmt, marker::PhantomData, net, rc::Rc, time::Duration};
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
use std::rc::Rc;
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||
use actix_server_config::ServerConfig as SrvConfig;
|
||||
use actix_service::{IntoNewService, NewService, Service};
|
||||
|
||||
use crate::{
|
||||
body::{BoxBody, MessageBody},
|
||||
h1::{self, ExpectHandler, H1Service, UpgradeHandler},
|
||||
service::HttpService,
|
||||
ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfig,
|
||||
};
|
||||
use crate::body::MessageBody;
|
||||
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;
|
||||
|
||||
/// An HTTP service builder.
|
||||
/// A http service builder
|
||||
///
|
||||
/// This type can construct an instance of [`HttpService`] through a builder-like pattern.
|
||||
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
||||
/// This type can be used to construct an instance of `http service` through a
|
||||
/// builder-like pattern.
|
||||
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
|
||||
keep_alive: KeepAlive,
|
||||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
secure: bool,
|
||||
local_addr: Option<net::SocketAddr>,
|
||||
client_timeout: u64,
|
||||
client_disconnect: u64,
|
||||
expect: X,
|
||||
upgrade: Option<U>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<S>,
|
||||
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
|
||||
_t: PhantomData<(T, S)>,
|
||||
}
|
||||
|
||||
impl<T, S> Default for HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler<T>>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S: NewService<Config = SrvConfig, Request = Request>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
fn default() -> Self {
|
||||
/// Create instance of `ServiceConfigBuilder`
|
||||
pub fn new() -> Self {
|
||||
HttpServiceBuilder {
|
||||
// ServiceConfig parts (make sure defaults match)
|
||||
keep_alive: KeepAlive::default(),
|
||||
client_request_timeout: Duration::from_secs(5),
|
||||
client_disconnect_timeout: Duration::ZERO,
|
||||
secure: false,
|
||||
local_addr: None,
|
||||
|
||||
// dispatcher parts
|
||||
keep_alive: KeepAlive::Timeout(5),
|
||||
client_timeout: 5000,
|
||||
client_disconnect: 0,
|
||||
expect: ExpectHandler,
|
||||
upgrade: None,
|
||||
on_connect_ext: None,
|
||||
_phantom: PhantomData,
|
||||
on_connect: None,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S: NewService<Config = SrvConfig, Request = Request>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X: NewService<Config = SrvConfig, Request = Request, Response = Request>,
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||
U: NewService<
|
||||
Config = SrvConfig,
|
||||
Request = (Request, Framed<T, Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
U::Error: fmt::Display,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Set connection keep-alive setting.
|
||||
/// Set server keep-alive setting.
|
||||
///
|
||||
/// Applies to HTTP/1.1 keep-alive and HTTP/2 ping-pong.
|
||||
///
|
||||
/// By default keep-alive is 5 seconds.
|
||||
/// By default keep alive is set to a 5 seconds.
|
||||
pub fn keep_alive<W: Into<KeepAlive>>(mut self, val: W) -> Self {
|
||||
self.keep_alive = val.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set connection secure state
|
||||
pub fn secure(mut self) -> Self {
|
||||
self.secure = true;
|
||||
/// Set server client timeout in milliseconds for first request.
|
||||
///
|
||||
/// Defines a timeout for reading client request header. If a client does not transmit
|
||||
/// the entire set headers within this time, the request is terminated with
|
||||
/// the 408 (Request Time-out) error.
|
||||
///
|
||||
/// To disable timeout set value to 0.
|
||||
///
|
||||
/// By default client timeout is set to 5000 milliseconds.
|
||||
pub fn client_timeout(mut self, val: u64) -> Self {
|
||||
self.client_timeout = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the local address that this service is bound to.
|
||||
pub fn local_addr(mut self, addr: net::SocketAddr) -> Self {
|
||||
self.local_addr = Some(addr);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set client request timeout (for first request).
|
||||
///
|
||||
/// Defines a timeout for reading client request header. If the client does not transmit the
|
||||
/// request head within this duration, the connection is terminated with a `408 Request Timeout`
|
||||
/// response error.
|
||||
///
|
||||
/// A duration of zero disables the timeout.
|
||||
///
|
||||
/// By default, the client timeout is 5 seconds.
|
||||
pub fn client_request_timeout(mut self, dur: Duration) -> Self {
|
||||
self.client_request_timeout = dur;
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "3.0.0", note = "Renamed to `client_request_timeout`.")]
|
||||
pub fn client_timeout(self, dur: Duration) -> Self {
|
||||
self.client_request_timeout(dur)
|
||||
}
|
||||
|
||||
/// Set client connection disconnect timeout.
|
||||
/// Set server connection disconnect timeout in milliseconds.
|
||||
///
|
||||
/// Defines a timeout for disconnect connection. If a disconnect procedure does not complete
|
||||
/// within this time, the request get dropped. This timeout affects secure connections.
|
||||
///
|
||||
/// A duration of zero disables the timeout.
|
||||
/// To disable timeout set value to 0.
|
||||
///
|
||||
/// By default, the disconnect timeout is disabled.
|
||||
pub fn client_disconnect_timeout(mut self, dur: Duration) -> Self {
|
||||
self.client_disconnect_timeout = dur;
|
||||
/// By default disconnect timeout is set to 0.
|
||||
pub fn client_disconnect(mut self, val: u64) -> Self {
|
||||
self.client_disconnect = val;
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "3.0.0", note = "Renamed to `client_disconnect_timeout`.")]
|
||||
pub fn client_disconnect(self, dur: Duration) -> Self {
|
||||
self.client_disconnect_timeout(dur)
|
||||
}
|
||||
|
||||
/// Provide service for `EXPECT: 100-Continue` support.
|
||||
///
|
||||
/// Service get called with request that contains `EXPECT` header.
|
||||
@ -131,21 +108,19 @@ 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, Request>,
|
||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X1::Error: Into<Response<BoxBody>>,
|
||||
F: IntoNewService<X1>,
|
||||
X1: NewService<Config = SrvConfig, Request = Request, Response = Request>,
|
||||
X1::Error: Into<Error>,
|
||||
X1::InitError: fmt::Debug,
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
client_request_timeout: self.client_request_timeout,
|
||||
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||
secure: self.secure,
|
||||
local_addr: self.local_addr,
|
||||
expect: expect.into_factory(),
|
||||
client_timeout: self.client_timeout,
|
||||
client_disconnect: self.client_disconnect,
|
||||
expect: expect.into_new_service(),
|
||||
upgrade: self.upgrade,
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,104 +130,96 @@ 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, (Request, Framed<T, h1::Codec>)>,
|
||||
U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||
F: IntoNewService<U1>,
|
||||
U1: NewService<
|
||||
Config = SrvConfig,
|
||||
Request = (Request, Framed<T, Codec>),
|
||||
Response = (),
|
||||
>,
|
||||
U1::Error: fmt::Display,
|
||||
U1::InitError: fmt::Debug,
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
keep_alive: self.keep_alive,
|
||||
client_request_timeout: self.client_request_timeout,
|
||||
client_disconnect_timeout: self.client_disconnect_timeout,
|
||||
secure: self.secure,
|
||||
local_addr: self.local_addr,
|
||||
client_timeout: self.client_timeout,
|
||||
client_disconnect: self.client_disconnect,
|
||||
expect: self.expect,
|
||||
upgrade: Some(upgrade.into_factory()),
|
||||
on_connect_ext: self.on_connect_ext,
|
||||
_phantom: PhantomData,
|
||||
upgrade: Some(upgrade.into_new_service()),
|
||||
on_connect: self.on_connect,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the callback to be run on connection establishment.
|
||||
/// Set on-connect callback.
|
||||
///
|
||||
/// 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
|
||||
/// 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
|
||||
where
|
||||
F: Fn(&T, &mut Extensions) + 'static,
|
||||
F: Fn(&T) -> I + 'static,
|
||||
I: Clone + 'static,
|
||||
{
|
||||
self.on_connect_ext = Some(Rc::new(f));
|
||||
self.on_connect = Some(Rc::new(move |io| Box::new(Data(f(io)))));
|
||||
self
|
||||
}
|
||||
|
||||
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol.
|
||||
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
|
||||
/// Finish service configuration and create *http service* for HTTP/1 protocol.
|
||||
pub fn h1<F, P, B>(self, service: F) -> H1Service<T, P, S, B, X, U>
|
||||
where
|
||||
B: MessageBody,
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
B: MessageBody + 'static,
|
||||
F: IntoNewService<S>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
self.client_timeout,
|
||||
self.client_disconnect,
|
||||
);
|
||||
|
||||
H1Service::with_config(cfg, service.into_factory())
|
||||
H1Service::with_config(cfg, service.into_new_service())
|
||||
.expect(self.expect)
|
||||
.upgrade(self.upgrade)
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
.on_connect(self.on_connect)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
||||
#[cfg(feature = "http2")]
|
||||
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||
/// Finish service configuration and create *http service* for HTTP/2 protocol.
|
||||
pub fn h2<F, P, B>(self, service: F) -> H2Service<T, P, S, B>
|
||||
where
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
F: IntoNewService<S>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
self.client_timeout,
|
||||
self.client_disconnect,
|
||||
);
|
||||
|
||||
crate::h2::H2Service::with_config(cfg, service.into_factory())
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
H2Service::with_config(cfg, service.into_new_service())
|
||||
.on_connect(self.on_connect)
|
||||
}
|
||||
|
||||
/// Finish service configuration and create `HttpService` instance.
|
||||
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
|
||||
pub fn finish<F, P, B>(self, service: F) -> HttpService<T, P, S, B, X, U>
|
||||
where
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
F: IntoNewService<S>,
|
||||
S::Error: Into<Error>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
<S::Service as Service>::Future: 'static,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
self.client_request_timeout,
|
||||
self.client_disconnect_timeout,
|
||||
self.secure,
|
||||
self.local_addr,
|
||||
self.client_timeout,
|
||||
self.client_disconnect,
|
||||
);
|
||||
|
||||
HttpService::with_config(cfg, service.into_factory())
|
||||
HttpService::with_config(cfg, service.into_new_service())
|
||||
.expect(self.expect)
|
||||
.upgrade(self.upgrade)
|
||||
.on_connect_ext(self.on_connect_ext)
|
||||
.on_connect(self.on_connect)
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user