diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3aeae6b1b..55289db86 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,8 +4,12 @@ updates: directory: / schedule: interval: weekly + cooldown: + default-days: 3 - package-ecosystem: cargo directory: / schedule: interval: weekly + cooldown: + default-days: 3 versioning-strategy: lockfile-only diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index cabf1c267..2a1de96d4 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -17,6 +17,8 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install Rust run: | diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index ee71934e8..5656978b0 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -29,6 +29,8 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install nasm if: matrix.target.os == 'windows-latest' @@ -49,7 +51,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -72,18 +74,20 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Free Disk Space run: ./scripts/free-disk-space.sh - name: Setup mold linker - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 - name: Install just, cargo-hack - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: just,cargo-hack diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48193b682..3f8165865 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,8 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install nasm if: matrix.target.os == 'windows-latest' @@ -56,7 +58,7 @@ jobs: - name: Setup mold linker if: matrix.target.os == 'ubuntu-latest' - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 + uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: Install Rust (${{ matrix.version.name }}) uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 @@ -64,7 +66,7 @@ jobs: toolchain: ${{ matrix.version.version }} - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean @@ -94,6 +96,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 @@ -110,6 +114,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install Rust (nightly) uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 @@ -117,7 +123,7 @@ jobs: toolchain: nightly - name: Install just - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: just diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f107793e6..0ed8bd7ee 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -16,6 +16,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install Rust (nightly) uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 @@ -24,7 +26,7 @@ jobs: components: llvm-tools - name: Install just, cargo-llvm-cov, cargo-nextest - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: just,cargo-llvm-cov,cargo-nextest diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 91d234abe..4a3f66a95 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -13,4 +13,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 64d9c60c1..91ce706e7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,10 +12,30 @@ concurrency: cancel-in-progress: true jobs: + zizmor: + name: zizmor + permissions: + actions: read + contents: read + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + with: + advanced-security: false + annotations: true + version: v1.24.1 + fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install Rust (nightly) uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 @@ -34,6 +54,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 @@ -53,6 +75,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install Rust (nightly) uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 @@ -70,6 +94,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }}) uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 @@ -77,14 +103,16 @@ jobs: toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} - name: Install just - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: just - name: Install cargo-check-external-types - uses: taiki-e/cache-cargo-install-action@f9eed3e4680f27610dc6d8c67be1b88593f7dade # v3.0.6 + uses: taiki-e/cache-cargo-install-action@417450f3c33ee20393705369577571770643d4c7 # v3.0.7 with: tool: cargo-check-external-types - name: check external types - run: just check-external-types-all +${{ vars.RUST_VERSION_EXTERNAL_TYPES }} + run: just check-external-types-all +"${RUST_VERSION_EXTERNAL_TYPES}" + env: + RUST_VERSION_EXTERNAL_TYPES: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }} diff --git a/.github/workflows/semver-checks.yml b/.github/workflows/semver-checks.yml index 3ac2675e1..2526f5ee8 100644 --- a/.github/workflows/semver-checks.yml +++ b/.github/workflows/semver-checks.yml @@ -13,6 +13,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + persist-credentials: false - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 @@ -20,7 +21,7 @@ jobs: toolchain: stable - name: Install cargo-semver-checks - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: tool: cargo-semver-checks @@ -59,16 +60,22 @@ jobs: - name: Summarize cargo semver-checks output if: always() && steps.semver.outcome != 'skipped' shell: bash + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + SEMVER_TYPE: ${{ steps.semver.outputs.semver_type }} + STATUS: ${{ steps.semver.outputs.exit_code }} + SUMMARY_FILE: ${{ steps.semver.outputs.output_file }} run: | - summary_file="${{ steps.semver.outputs.output_file }}" - status="${{ steps.semver.outputs.exit_code }}" + summary_file="$SUMMARY_FILE" + status="$STATUS" { echo "## cargo semver-checks" echo - echo "- Base SHA: \`${{ github.event.pull_request.base.sha }}\`" - echo "- Head SHA: \`${{ github.event.pull_request.head.sha }}\`" - echo "- Required release: \`${{ steps.semver.outputs.semver_type }}\`" + echo "- Base SHA: \`${BASE_SHA}\`" + echo "- Head SHA: \`${HEAD_SHA}\`" + echo "- Required release: \`${SEMVER_TYPE}\`" echo "- cargo semver-checks exit code: \`$status\`" echo diff --git a/Cargo.lock b/Cargo.lock index 483994aa5..7ff301999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,7 +294,7 @@ dependencies = [ "futures-core", "http 0.2.12", "http 1.4.0", - "impl-more", + "impl-more 0.1.9", "openssl", "pin-project-lite", "rustls-native-certs", @@ -354,7 +354,7 @@ dependencies = [ "foldhash 0.2.0", "futures-core", "futures-util", - "impl-more", + "impl-more 0.3.1", "itoa", "language-tags", "log", @@ -465,6 +465,15 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "anes" version = "0.1.6" @@ -726,9 +735,9 @@ checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" [[package]] name = "bytestring" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" dependencies = [ "bytes", ] @@ -846,6 +855,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "common-multipart-rfc7578" version = "0.7.0" @@ -907,6 +926,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -963,25 +992,24 @@ dependencies = [ [[package]] name = "criterion" -version = "0.5.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", "itertools", "num-traits", - "once_cell", "oorandom", + "page_size", "plotters", "rayon", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -989,9 +1017,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools", @@ -1074,9 +1102,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -1084,11 +1112,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -1098,9 +1125,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", @@ -1224,18 +1251,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "env_filter" version = "1.0.1" @@ -1435,6 +1450,7 @@ checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1563,23 +1579,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] -name = "hickory-proto" -version = "0.25.2" +name = "hickory-net" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner", "futures-channel", "futures-io", "futures-util", + "hickory-proto", "idna", "ipnet", - "once_cell", - "rand 0.9.4", - "ring 0.17.14", + "jni", + "rand 0.10.1", "thiserror 2.0.18", "tinyvec", "tokio", @@ -1588,21 +1603,46 @@ dependencies = [ ] [[package]] -name = "hickory-resolver" -version = "0.25.2" +name = "hickory-proto" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring 0.17.14", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" dependencies = [ "cfg-if", "futures-util", + "hickory-net", "hickory-proto", "ipconfig", + "ipnet", + "jni", "moka", + "ndk-context", "once_cell", "parking_lot", - "rand 0.9.4", + "rand 0.10.1", "resolv-conf", "smallvec", + "system-configuration", "thiserror 2.0.18", "tokio", "tracing", @@ -1795,6 +1835,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +[[package]] +name = "impl-more" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870" + [[package]] name = "indexmap" version = "2.14.0" @@ -1844,16 +1890,8 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.61.2", + "serde", ] [[package]] @@ -1864,9 +1902,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.10.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -1901,6 +1939,55 @@ dependencies = [ "syn", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -2073,6 +2160,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "num-conv" version = "0.2.1" @@ -2128,15 +2221,14 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.78" +version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -2160,9 +2252,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.114" +version = "0.9.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" dependencies = [ "cc", "libc", @@ -2170,6 +2262,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2326,6 +2428,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prefix-trie" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23370be78b7e5bcbb0cab4a02047eb040279a693c78daad04c2c5f1c24a83503" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -2373,20 +2486,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - [[package]] name = "rand" version = "0.10.1" @@ -2408,16 +2511,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -2427,15 +2520,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - [[package]] name = "rand_core" version = "0.10.1" @@ -2652,9 +2736,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] @@ -2751,7 +2835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2890,6 +2974,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -2990,6 +3090,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -3434,10 +3555,19 @@ dependencies = [ ] [[package]] -name = "v_htmlescape" -version = "0.15.8" +name = "v_escape-base" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" +checksum = "f1212fce830b75af194b578e55b3db9049f2c8c45f58d397fb25602fdb50fb3d" + +[[package]] +name = "v_htmlescape" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befb3d53c9e3ec641417685896cbc8cc5bd264d6a2e190c56aaef1af24740d99" +dependencies = [ + "v_escape-base", +] [[package]] name = "vcpkg" diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 6f4056f57..aea6fe8f4 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -2,11 +2,14 @@ ## Unreleased +- Add support for passing multiple root directories to `Files::new`. [#3402] - Add `Files::try_compressed()` to support serving pre-compressed static files [#2615] - Fix handling of `bytes=0-` - Fix `NamedFile` panic when serving files with pre-UNIX epoch modification times. [#2748] - Fix invalid `Content-Encoding: identity` header in `NamedFile` range responses. [#3191] +- Update `v_htmlescape` dependency to `0.17`. +[#3402]: https://github.com/actix/actix-web/issues/3402 [#2615]: https://github.com/actix/actix-web/pull/2615 [#2748]: https://github.com/actix/actix-web/issues/2748 [#3191]: https://github.com/actix/actix-web/issues/3191 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 65e91cafd..bfb49d4b3 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -32,7 +32,7 @@ mime = "0.3.9" mime_guess = "2.0.1" percent-encoding = "2.1" pin-project-lite = "0.2.7" -v_htmlescape = "0.15.5" +v_htmlescape = "0.17" # experimental-io-uring [target.'cfg(target_os = "linux")'.dependencies] diff --git a/actix-files/src/directory.rs b/actix-files/src/directory.rs index 6ade424b9..9513f6c06 100644 --- a/actix-files/src/directory.rs +++ b/actix-files/src/directory.rs @@ -7,7 +7,7 @@ use std::{ use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; use percent_encoding::{utf8_percent_encode, CONTROLS}; -use v_htmlescape::escape as escape_html_entity; +use v_htmlescape::escape_fmt; /// A directory; responds with the generated directory listing. #[derive(Debug)] @@ -64,7 +64,7 @@ macro_rules! encode_file_url { /// ``` macro_rules! encode_file_name { ($entry:ident) => { - escape_html_entity(&$entry.file_name().to_string_lossy()) + escape_fmt(&$entry.file_name().to_string_lossy()) }; } diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 1c7a1d902..8145d614a 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -1,5 +1,7 @@ use std::{ + borrow::Cow, cell::RefCell, + ffi::{OsStr, OsString}, fmt, io, path::{Path, PathBuf}, rc::Rc, @@ -37,7 +39,7 @@ use crate::{ /// ``` pub struct Files { mount_path: String, - directory: PathBuf, + directories: Vec, index: Option, show_index: bool, redirect_to_slash: bool, @@ -63,7 +65,7 @@ impl fmt::Debug for Files { impl Clone for Files { fn clone(&self) -> Self { Self { - directory: self.directory.clone(), + directories: self.directories.clone(), index: self.index.clone(), show_index: self.show_index, redirect_to_slash: self.redirect_to_slash, @@ -83,6 +85,131 @@ impl Clone for Files { } } +/// File serving root directories for [`Files`]. +/// +/// This type is used by [`Files::new`] to accept either one root directory or an ordered +/// collection of root directories. +#[derive(Debug)] +pub struct FilesDirs(Vec); + +impl FilesDirs { + fn canonicalize(self) -> Vec { + self.0 + .into_iter() + .map(|orig_dir| match orig_dir.canonicalize() { + Ok(canon_dir) => canon_dir, + Err(_) => { + log::error!("Specified path is not a directory: {:?}", orig_dir); + // Preserve original path so requests don't fall back to CWD. + orig_dir + } + }) + .collect() + } +} + +impl From<&Path> for FilesDirs { + fn from(dir: &Path) -> Self { + Self(vec![dir.into()]) + } +} + +impl From<&PathBuf> for FilesDirs { + fn from(dir: &PathBuf) -> Self { + Self(vec![dir.into()]) + } +} + +impl From for FilesDirs { + fn from(dir: PathBuf) -> Self { + Self(vec![dir]) + } +} + +impl From<&str> for FilesDirs { + fn from(dir: &str) -> Self { + Self(vec![dir.into()]) + } +} + +impl From<&String> for FilesDirs { + fn from(dir: &String) -> Self { + Self(vec![dir.into()]) + } +} + +impl From for FilesDirs { + fn from(dir: String) -> Self { + Self(vec![dir.into()]) + } +} + +impl From<&OsStr> for FilesDirs { + fn from(dir: &OsStr) -> Self { + Self(vec![dir.into()]) + } +} + +impl From for FilesDirs { + fn from(dir: OsString) -> Self { + Self(vec![dir.into()]) + } +} + +impl From<&OsString> for FilesDirs { + fn from(dir: &OsString) -> Self { + Self(vec![dir.into()]) + } +} + +impl From> for FilesDirs { + fn from(dir: Box) -> Self { + Self(vec![dir.into()]) + } +} + +impl From> for FilesDirs { + fn from(dir: Cow<'_, Path>) -> Self { + Self(vec![dir.into()]) + } +} + +impl From<[P; N]> for FilesDirs +where + P: Into, +{ + fn from(dirs: [P; N]) -> Self { + Self(dirs.into_iter().map(Into::into).collect()) + } +} + +impl From<&[P; N]> for FilesDirs +where + P: Clone + Into, +{ + fn from(dirs: &[P; N]) -> Self { + Self(dirs.iter().cloned().map(Into::into).collect()) + } +} + +impl

From<&[P]> for FilesDirs +where + P: Clone + Into, +{ + fn from(dirs: &[P]) -> Self { + Self(dirs.iter().cloned().map(Into::into).collect()) + } +} + +impl

From> for FilesDirs +where + P: Into, +{ + fn from(dirs: Vec

) -> Self { + Self(dirs.into_iter().map(Into::into).collect()) + } +} + impl Files { /// Create new `Files` instance for a specified base directory. /// @@ -90,34 +217,34 @@ impl Files { /// 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. + /// The second argument (`serve_from`) is the location on disk that files are served from. This + /// can be a single path or an ordered collection of paths. Relative paths are resolved from the + /// current working directory. + /// + /// When multiple directories are provided, they are checked in order. The first directory that + /// can serve the requested path is used. + /// + /// Directory listings are generated from the first matching directory and are not merged across + /// roots. When [`Files::index_file()`] is configured, later roots are searched if an earlier + /// matching directory does not contain the index file. + /// + /// Empty root collections never match files; requests fall through to the default handler, or + /// return `404 Not Found` if none is configured. /// /// # 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. /// - /// If `serve_from` cannot be canonicalized at startup, an error is logged and the original - /// path is preserved. Requests will return `404 Not Found` until the path exists. + /// If a `serve_from` path cannot be canonicalized at startup, an error is logged and the + /// original path is preserved. Requests will return `404 Not Found` until the path exists. /// /// `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>(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); - // Preserve original path so requests don't fall back to CWD. - orig_dir - } - }; - + pub fn new>(mount_path: &str, serve_from: T) -> Files { Files { mount_path: mount_path.trim_end_matches('/').to_owned(), - directory: dir, + directories: serve_from.into().canonicalize(), index: None, show_index: false, redirect_to_slash: false, @@ -149,6 +276,9 @@ impl Files { /// Redirects to a slash-ended path when browsing a directory. /// /// By default never redirect. + /// + /// When multiple root directories are configured, a matching directory in an earlier root can + /// trigger a redirect before later roots are checked for a file at the same path. pub fn redirect_to_slash_directory(mut self) -> Self { self.redirect_to_slash = true; self @@ -407,7 +537,7 @@ impl ServiceFactory for Files { fn new_service(&self, _: ()) -> Self::Future { let mut inner = FilesServiceInner { - directory: self.directory.clone(), + directories: self.directories.clone(), index: self.index.clone(), show_index: self.show_index, redirect_to_slash: self.redirect_to_slash, diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index bf5397ecf..3312a621d 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -37,8 +37,14 @@ mod range; mod service; pub use self::{ - chunked::ChunkedReadFile, directory::Directory, error::UriSegmentError, files::Files, - named::NamedFile, path_buf::PathBufWrap, range::HttpRange, service::FilesService, + chunked::ChunkedReadFile, + directory::Directory, + error::UriSegmentError, + files::{Files, FilesDirs}, + named::NamedFile, + path_buf::PathBufWrap, + range::HttpRange, + service::FilesService, }; use self::{ directory::{directory_listing, DirectoryRenderer}, @@ -63,9 +69,11 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool; #[cfg(test)] mod tests { use std::{ + ffi::OsString, fmt::Write as _, fs::{self}, ops::Add, + path::PathBuf, time::{Duration, SystemTime}, }; @@ -832,6 +840,243 @@ mod tests { assert_eq!(resp.status(), StatusCode::NOT_FOUND); } + #[actix_rt::test] + async fn test_static_files_accepts_borrowed_os_string_directory() { + let dir = OsString::from("."); + let service = Files::new("/", &dir).new_service(()).await.unwrap(); + + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&service, req).await; + + assert_eq!(resp.status(), StatusCode::OK); + } + + #[actix_rt::test] + async fn test_static_files_empty_directories() { + let service = Files::new("/", Vec::::new()) + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let service = Files::new("/", Vec::::new()) + .default_handler(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::Ok().body("default content"))) + }) + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + test::read_body(resp).await, + Bytes::from_static(b"default content") + ); + } + + #[actix_rt::test] + async fn test_static_files_multiple_directories() { + let first_dir = tempfile::tempdir().unwrap(); + let second_dir = tempfile::tempdir().unwrap(); + + fs::write(first_dir.path().join("shared.txt"), "first").unwrap(); + fs::write(second_dir.path().join("shared.txt"), "second").unwrap(); + fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap(); + + let service = Files::new("/", [first_dir.path(), second_dir.path()]) + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/shared.txt").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(test::read_body(resp).await, Bytes::from_static(b"first")); + + let req = TestRequest::with_uri("/fallback.txt").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback")); + } + + #[actix_rt::test] + async fn test_static_files_multiple_directories_file_as_parent_falls_back() { + let first_dir = tempfile::tempdir().unwrap(); + let second_dir = tempfile::tempdir().unwrap(); + + fs::write(first_dir.path().join("assets"), "file").unwrap(); + fs::create_dir(second_dir.path().join("assets")).unwrap(); + fs::write( + second_dir.path().join("assets").join("fallback.txt"), + "fallback", + ) + .unwrap(); + + let service = Files::new("/", [first_dir.path(), second_dir.path()]) + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/assets/fallback.txt").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback")); + } + + #[actix_rt::test] + async fn test_static_files_multiple_directories_default_handler() { + let first_dir = tempfile::tempdir().unwrap(); + let second_dir = tempfile::tempdir().unwrap(); + + fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap(); + + let service = Files::new("/", vec![first_dir.path(), second_dir.path()]) + .default_handler(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::Ok().body("default content"))) + }) + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/fallback.txt").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback")); + + let req = TestRequest::with_uri("/missing.txt").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + test::read_body(resp).await, + Bytes::from_static(b"default content") + ); + } + + #[actix_rt::test] + async fn test_static_files_multiple_directories_index_file() { + let first_dir = tempfile::tempdir().unwrap(); + let second_dir = tempfile::tempdir().unwrap(); + + fs::create_dir(first_dir.path().join("nested")).unwrap(); + fs::create_dir(second_dir.path().join("nested")).unwrap(); + fs::write( + second_dir.path().join("nested").join("index.html"), + "second index", + ) + .unwrap(); + + let service = Files::new("/", [first_dir.path(), second_dir.path()]) + .index_file("index.html") + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/nested/").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + test::read_body(resp).await, + Bytes::from_static(b"second index") + ); + } + + #[actix_rt::test] + async fn test_static_files_multiple_directories_index_file_as_parent_falls_back() { + let first_dir = tempfile::tempdir().unwrap(); + let second_dir = tempfile::tempdir().unwrap(); + + fs::create_dir(first_dir.path().join("nested")).unwrap(); + fs::write(first_dir.path().join("nested").join("index.html"), "file").unwrap(); + fs::create_dir(second_dir.path().join("nested")).unwrap(); + fs::create_dir(second_dir.path().join("nested").join("index.html")).unwrap(); + fs::write( + second_dir + .path() + .join("nested") + .join("index.html") + .join("fallback.txt"), + "fallback", + ) + .unwrap(); + + let service = Files::new("/", [first_dir.path(), second_dir.path()]) + .index_file("index.html/fallback.txt") + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/nested/").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback")); + } + + #[actix_rt::test] + async fn test_static_files_index_file_error_falls_back_to_listing() { + let dir = tempfile::tempdir().unwrap(); + + fs::write(dir.path().join("listed.txt"), "listed").unwrap(); + + let service = Files::new("/", dir.path()) + .index_file("index.html\0") + .show_files_listing() + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + let bytes = test::read_body(resp).await; + assert!(format!("{bytes:?}").contains("listed.txt")); + } + + #[actix_rt::test] + async fn test_static_files_multiple_directories_show_files_listing() { + let first_dir = tempfile::tempdir().unwrap(); + let second_dir = tempfile::tempdir().unwrap(); + + fs::write(first_dir.path().join("listed.txt"), "listed").unwrap(); + + let service = Files::new("/", [first_dir.path(), second_dir.path()]) + .show_files_listing() + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + let bytes = test::read_body(resp).await; + assert!(format!("{bytes:?}").contains("listed.txt")); + } + + #[actix_rt::test] + async fn test_static_files_multiple_directories_redirect_precedence() { + let first_dir = tempfile::tempdir().unwrap(); + let second_dir = tempfile::tempdir().unwrap(); + + fs::create_dir(first_dir.path().join("item")).unwrap(); + fs::write(second_dir.path().join("item"), "file").unwrap(); + + let service = Files::new("/", [first_dir.path(), second_dir.path()]) + .show_files_listing() + .redirect_to_slash_directory() + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/item").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT); + assert_eq!(resp.headers().get(header::LOCATION).unwrap(), "/item/"); + } + #[actix_rt::test] async fn test_default_handler_file_missing() { let st = Files::new("/", ".") diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index f1a4642d7..f5d3df4e7 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -117,14 +117,35 @@ pub(crate) fn get_content_type_and_disposition( }; // replace special characters in filenames which could occur on some filesystems - let filename_s = filename - .replace('\n', "%0A") // \n line break - .replace('\x0B', "%0B") // \v vertical tab - .replace('\x0C', "%0C") // \f form feed - .replace('\r', "%0D"); // \r carriage return - let mut parameters = vec![DispositionParam::Filename(filename_s)]; + let mut escaped_len = filename.len(); + for byte in filename.bytes() { + if matches!(byte, b'\n' | b'\x0B' | b'\x0C' | b'\r') { + escaped_len += 2; + } + } - if !filename.is_ascii() { + let filename_s = if escaped_len == filename.len() { + filename.to_string() + } else { + let mut escaped = String::with_capacity(escaped_len); + for ch in filename.chars() { + match ch { + '\n' => escaped.push_str("%0A"), // \n line break + '\x0B' => escaped.push_str("%0B"), // \v vertical tab + '\x0C' => escaped.push_str("%0C"), // \f form feed + '\r' => escaped.push_str("%0D"), // \r carriage return + ch => escaped.push(ch), + } + } + escaped + }; + + let is_ascii = filename.is_ascii(); + + let mut parameters = Vec::with_capacity(if is_ascii { 1 } else { 2 }); + parameters.push(DispositionParam::Filename(filename_s)); + + if !is_ascii { parameters.push(DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Ext(String::from("UTF-8")), language_tag: None, @@ -735,4 +756,15 @@ mod tests { let (_ct, cd) = get_content_type_and_disposition(Path::new("sound.mp3")).unwrap(); assert_eq!(cd.disposition, DispositionType::Inline); } + + #[test] + fn special_chars_are_escaped_in_content_disposition_filename() { + let (_ct, cd) = + get_content_type_and_disposition(Path::new("test\n\x0B\x0C\rnewline.text")).unwrap(); + + assert_eq!( + cd.to_string(), + "inline; filename=\"test%0A%0B%0C%0Dnewline.text\"", + ); + } } diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 0fe8493bf..f12d68593 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, path::{Component, Path, PathBuf}, str::FromStr, }; @@ -70,8 +71,10 @@ impl PathBufWrap { .map_err(|_| UriSegmentError::NotValidUtf8)?; // disallow decoding `%2F` into `/` - if segment_count != path.matches('/').count() + 1 { - return Err(UriSegmentError::BadChar('/')); + if let Cow::Owned(ref path) = path { + if segment_count != path.matches('/').count() + 1 { + return Err(UriSegmentError::BadChar('/')); + } } for segment in path.split('/') { @@ -199,6 +202,14 @@ mod tests { ); } + #[test] + fn encoded_slash_is_rejected() { + assert_eq!( + PathBufWrap::parse_path("/test%2Ffile.txt", false), + Err(UriSegmentError::BadChar('/')) + ); + } + #[test] #[cfg_attr(windows, should_panic)] fn windows_drive_traversal() { diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index ae6725385..0dccee510 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -33,7 +33,7 @@ impl Deref for FilesService { } pub struct FilesServiceInner { - pub(crate) directory: PathBuf, + pub(crate) directories: Vec, pub(crate) index: Option, pub(crate) show_index: bool, pub(crate) redirect_to_slash: bool, @@ -113,8 +113,8 @@ impl FilesService { self.serve_named_file_with_encoding(req, named_file, header::ContentEncoding::Identity) } - fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse { - let dir = Directory::new(self.directory.clone(), path); + fn show_index(&self, req: ServiceRequest, base: PathBuf, path: PathBuf) -> ServiceResponse { + let dir = Directory::new(base, path); let (req, _) = req.into_parts(); @@ -171,70 +171,124 @@ impl Service for FilesService { } } - // full file path - let path = this.directory.join(&path_on_disk); + let mut last_miss = None; + let mut first_index_listing = None; + let mut found_unrenderable_dir = false; - // Try serving pre-compressed file even if the uncompressed file doesn't exist yet. - // Still handle directories (index/listing) through the normal branch below. - if this.try_compressed && !path.is_dir() { - if let Some((named_file, encoding)) = find_compressed(&req, &path).await { - return Ok(this.serve_named_file_with_encoding(req, named_file, encoding)); - } - } + for directory in &this.directories { + // full file path + let path = 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()); - - let response = if this.with_permanent_redirect { - HttpResponse::PermanentRedirect() - } else { - HttpResponse::TemporaryRedirect() + // Try serving pre-compressed file even if the uncompressed file doesn't exist yet. + // Still handle directories (index/listing) through the normal branch below. + if this.try_compressed && !path.is_dir() { + if let Some((named_file, encoding)) = find_compressed(&req, &path).await { + return Ok(this.serve_named_file_with_encoding(req, named_file, encoding)); } - .insert_header((header::LOCATION, redirect_to)) - .finish(); - - return Ok(req.into_response(response)); } - match this.index { - Some(ref index) => { - let named_path = path.join(index); - if this.try_compressed { - if let Some((named_file, encoding)) = - find_compressed(&req, &named_path).await - { - return Ok( - this.serve_named_file_with_encoding(req, named_file, encoding) - ); + if let Err(err) = path.canonicalize() { + if matches!( + err.kind(), + io::ErrorKind::NotFound | io::ErrorKind::NotADirectory + ) { + last_miss = Some(err); + continue; + } + + return this.handle_err(err, req).await; + } + + if path.is_dir() { + if this.redirect_to_slash + && !req.path().ends_with('/') + && (this.index.is_some() || this.show_index) + { + let redirect_to = format!("{}/", req.path()); + + let response = if this.with_permanent_redirect { + HttpResponse::PermanentRedirect() + } else { + HttpResponse::TemporaryRedirect() + } + .insert_header((header::LOCATION, redirect_to)) + .finish(); + + return Ok(req.into_response(response)); + } + + match &this.index { + Some(index) => { + let named_path = path.join(index); + if this.try_compressed { + if let Some((named_file, encoding)) = + find_compressed(&req, &named_path).await + { + return Ok(this.serve_named_file_with_encoding( + req, named_file, encoding, + )); + } + } + // fallback to the uncompressed version + match NamedFile::open_async(named_path).await { + Ok(named_file) => return Ok(this.serve_named_file(req, named_file)), + Err(err) + if matches!( + err.kind(), + io::ErrorKind::NotFound | io::ErrorKind::NotADirectory + ) => + { + if this.show_index && first_index_listing.is_none() { + first_index_listing = + Some((directory.to_path_buf(), path.clone())); + } + last_miss = Some(err); + } + Err(_) if this.show_index => { + if first_index_listing.is_none() { + first_index_listing = + Some((directory.to_path_buf(), path.clone())); + } + break; + } + Err(err) => return this.handle_err(err, req).await, } } - // fallback to the uncompressed version - 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 => { + return Ok(this.show_index(req, directory.to_path_buf(), path)); } + None => found_unrenderable_dir = true, + } + } else { + match NamedFile::open_async(&path).await { + Ok(named_file) => return Ok(this.serve_named_file(req, named_file)), + Err(err) + if matches!( + err.kind(), + io::ErrorKind::NotFound | io::ErrorKind::NotADirectory + ) => + { + last_miss = Some(err); + } + Err(err) => return 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(named_file) => Ok(this.serve_named_file(req, named_file)), - Err(err) => this.handle_err(err, req).await, } } + + if let Some((base, path)) = first_index_listing { + return Ok(this.show_index(req, base, path)); + } + + if found_unrenderable_dir { + return Ok(ServiceResponse::from_err( + FilesError::IsDirectory, + req.into_parts().0, + )); + } + + let err = last_miss + .unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No such file")); + this.handle_err(err, req).await }) } } diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index 72e72b913..764648a84 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -166,6 +166,44 @@ async fn test_compression_encodings() { assert_eq!(res.headers().get(header::CONTENT_ENCODING), None); } +#[actix_web::test] +async fn test_compression_encodings_multiple_directories() { + use actix_web::body::MessageBody; + + let first_dir = tempfile::tempdir().unwrap(); + let second_dir = tempfile::tempdir().unwrap(); + + let compressed_path = second_dir.path().join("fallback.txt.gz"); + std::fs::write(&compressed_path, b"compressed").unwrap(); + let compressed_len = std::fs::metadata(compressed_path).unwrap().len(); + + let srv = test::init_service( + App::new().service(Files::new("/", [first_dir.path(), second_dir.path()]).try_compressed()), + ) + .await; + + let mut req = TestRequest::with_uri("/fallback.txt").to_request(); + req.headers_mut().insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("gzip"), + ); + 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")), + ); + assert_eq!( + res.headers().get(header::CONTENT_ENCODING), + Some(&HeaderValue::from_static("gzip")), + ); + assert_eq!( + res.into_body().size(), + actix_web::body::BodySize::Sized(compressed_len), + ); +} + #[actix_web::test] async fn partial_range_response_encoding() { let srv = test::init_service(App::new().default_service(web::to(|| async { diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index bcc13abe0..6e4f8ec32 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,9 +3,11 @@ ## Unreleased - When configured, gracefully close HTTP/1 connections after early responses to unread request bodies. [#3967] +- Wake HTTP/1 payload receivers with an incomplete-payload error when the sender is dropped before EOF. [#3100] - Update `foldhash` dependency to `0.2`. [#3967]: https://github.com/actix/actix-web/issues/3967 +[#3100]: https://github.com/actix/actix-web/issues/3100 ## 3.12.1 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index c76913cc5..7e28f6792 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -142,7 +142,7 @@ actix-web = "4" awc = { version = "3", default-features = false, features = ["openssl"] } async-stream = "0.3" -criterion = { version = "0.5", features = ["html_reports"] } +criterion = { version = "0.8", features = ["html_reports"] } divan = "0.1.8" env_logger = "0.11" futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } diff --git a/actix-http/benches/response-body-compression.rs b/actix-http/benches/response-body-compression.rs index 53279e312..04b4b5ea9 100644 --- a/actix-http/benches/response-body-compression.rs +++ b/actix-http/benches/response-body-compression.rs @@ -1,8 +1,8 @@ -use std::convert::Infallible; +use std::{convert::Infallible, hint::black_box}; use actix_http::{encoding::Encoder, ContentEncoding, Request, Response, StatusCode}; use actix_service::{fn_service, Service as _}; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; static BODY: &[u8] = include_bytes!("../Cargo.toml"); diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 6ef48b038..f28e91f31 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -366,10 +366,9 @@ where io.poll_flush(cx) } - fn enter_linger(mut self: Pin<&mut Self>) { - let this = self.as_mut().project(); - this.flags.remove(Flags::KEEP_ALIVE); - this.flags.insert(Flags::LINGER | Flags::FINISHED); + fn enter_linger(flags: &mut Flags) { + flags.remove(Flags::KEEP_ALIVE); + flags.insert(Flags::LINGER | Flags::FINISHED); } fn ensure_linger_timer(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> bool { @@ -464,23 +463,19 @@ where let size = self.as_mut().send_response_inner(res, &body)?; match size { BodySize::None | BodySize::Sized(0) => { - let this = self.as_mut().project(); + let mut this = self.as_mut().project(); if close_after_response { if this.config.client_disconnect_deadline().is_some() { - drop(this); - self.as_mut().enter_linger(); + Self::enter_linger(this.flags); } else { - self.as_mut() - .project() - .flags - .insert(Flags::SHUTDOWN | Flags::FINISHED); + this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED); } } else { this.flags.insert(Flags::FINISHED); } - self.as_mut().project().state.set(State::None); + this.state.set(State::None); } _ => self .as_mut() @@ -509,23 +504,19 @@ where let size = self.as_mut().send_response_inner(res, &body)?; match size { BodySize::None | BodySize::Sized(0) => { - let this = self.as_mut().project(); + let mut this = self.as_mut().project(); if close_after_response { if this.config.client_disconnect_deadline().is_some() { - drop(this); - self.as_mut().enter_linger(); + Self::enter_linger(this.flags); } else { - self.as_mut() - .project() - .flags - .insert(Flags::SHUTDOWN | Flags::FINISHED); + this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED); } } else { this.flags.insert(Flags::FINISHED); } - self.as_mut().project().state.set(State::None); + this.state.set(State::None); } _ => self .as_mut() @@ -646,13 +637,9 @@ where if not_pipelined && close_after_response { if this.config.client_disconnect_deadline().is_some() { - drop(this); - self.as_mut().enter_linger(); + Self::enter_linger(this.flags); } else { - self.as_mut() - .project() - .flags - .insert(Flags::SHUTDOWN | Flags::FINISHED); + this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED); } } else { this.flags.insert(Flags::FINISHED); @@ -708,13 +695,9 @@ where if not_pipelined && close_after_response { if this.config.client_disconnect_deadline().is_some() { - drop(this); - self.as_mut().enter_linger(); + Self::enter_linger(this.flags); } else { - self.as_mut() - .project() - .flags - .insert(Flags::SHUTDOWN | Flags::FINISHED); + this.flags.insert(Flags::SHUTDOWN | Flags::FINISHED); } } else { this.flags.insert(Flags::FINISHED); diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index e12c87806..3a1293bb8 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -140,11 +140,20 @@ impl PayloadSender { } } +impl Drop for PayloadSender { + fn drop(&mut self) { + if let Some(shared) = self.inner.upgrade() { + shared.borrow_mut().close_sender(); + } + } +} + #[derive(Debug)] struct Inner { len: usize, eof: bool, err: Option, + sender_closed: bool, need_read: bool, items: VecDeque, task: Option, @@ -157,6 +166,7 @@ impl Inner { eof, len: 0, err: None, + sender_closed: eof, items: VecDeque::new(), need_read: true, task: None, @@ -200,12 +210,21 @@ impl Inner { #[inline] fn set_error(&mut self, err: PayloadError) { + self.sender_closed = true; self.err = Some(err); self.wake(); } + fn close_sender(&mut self) { + if !self.sender_closed { + self.sender_closed = true; + self.set_error(PayloadError::Incomplete(None)); + } + } + #[inline] fn feed_eof(&mut self) { + self.sender_closed = true; self.eof = true; self.wake(); } @@ -332,6 +351,16 @@ mod tests { timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap(); } + #[actix_rt::test] + async fn wake_on_sender_drop() { + let (sender, payload) = Payload::create(false); + let (rx, handle) = prepare_waking_test(payload, Some(Err(()))); + + rx.await.unwrap(); + drop(sender); + timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap(); + } + #[actix_rt::test] async fn test_unread_data() { let (_, mut payload) = Payload::create(false); diff --git a/actix-multipart-derive/CHANGES.md b/actix-multipart-derive/CHANGES.md index a5f30757c..fa3f0a55d 100644 --- a/actix-multipart-derive/CHANGES.md +++ b/actix-multipart-derive/CHANGES.md @@ -5,6 +5,7 @@ ## 0.8.0 - Minimum supported Rust version (MSRV) is now 1.88. +- Update `darling` dependency to `0.23`. ## 0.7.0 diff --git a/actix-multipart-derive/Cargo.toml b/actix-multipart-derive/Cargo.toml index aee19b388..6d64568e6 100644 --- a/actix-multipart-derive/Cargo.toml +++ b/actix-multipart-derive/Cargo.toml @@ -18,7 +18,7 @@ proc-macro = true [dependencies] bytesize = "2" -darling = "0.20" +darling = "0.23" proc-macro2 = "1" quote = "1" syn = "2" diff --git a/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr b/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr index d25e43525..f010ceb76 100644 --- a/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr +++ b/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr @@ -1,4 +1,4 @@ -error: Unknown literal value `no` +error: Unknown value: `no`. Available values: `deny`, `ignore`, `replace` --> tests/trybuild/deny-parse-fail.rs:4:31 | 4 | #[multipart(duplicate_field = "no")] diff --git a/actix-multipart/src/multipart.rs b/actix-multipart/src/multipart.rs index bde7d122f..bd262c2ab 100644 --- a/actix-multipart/src/multipart.rs +++ b/actix-multipart/src/multipart.rs @@ -47,7 +47,7 @@ enum Flow { /// [`Multipart`] extractor configuration. /// /// Add to your app data to have it picked up by [`Multipart`] extractors. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] #[non_exhaustive] pub struct MultipartConfig { buffer_limit: usize, @@ -74,7 +74,7 @@ impl MultipartConfig { } } -static DEFAULT_CONFIG: MultipartConfig = MultipartConfig { +const DEFAULT_CONFIG: MultipartConfig = MultipartConfig { buffer_limit: DEFAULT_BUFFER_LIMIT, }; @@ -1013,7 +1013,7 @@ mod tests { #[actix_rt::test] async fn test_multipart_payload_consumption() { // with sample payload and HttpRequest with no headers - let (_, inner_payload) = h1::Payload::create(false); + let (_sender, inner_payload) = h1::Payload::create(false); let mut payload = actix_web::dev::Payload::from(inner_payload); let req = TestRequest::default().to_http_request(); diff --git a/actix-multipart/src/payload.rs b/actix-multipart/src/payload.rs index 4c9929aed..5fd0aa790 100644 --- a/actix-multipart/src/payload.rs +++ b/actix-multipart/src/payload.rs @@ -231,7 +231,7 @@ mod tests { #[actix_rt::test] async fn basic() { - let (_, payload) = h1::Payload::create(false); + let (_sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new_with_limit(payload, DEFAULT_BUFFER_LIMIT); assert_eq!(payload.buf.len(), 0); diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index 38be79944..f7fe3643a 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -30,7 +30,7 @@ serde = "1" tracing = { version = "0.1.30", default-features = false, features = ["log"] } [dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } +criterion = { version = "0.8", features = ["html_reports"] } http = "0.2.7" percent-encoding = "2.1" serde = { version = "1", features = ["derive"] } diff --git a/actix-router/benches/quoter.rs b/actix-router/benches/quoter.rs index 2428a767d..2143a066b 100644 --- a/actix-router/benches/quoter.rs +++ b/actix-router/benches/quoter.rs @@ -1,6 +1,6 @@ -use std::{borrow::Cow, fmt::Write as _}; +use std::{borrow::Cow, fmt::Write as _, hint::black_box}; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; fn compare_quoters(c: &mut Criterion) { let mut group = c.benchmark_group("Compare Quoters"); diff --git a/actix-router/benches/router.rs b/actix-router/benches/router.rs index 2c21fef6c..0163f6cbf 100644 --- a/actix-router/benches/router.rs +++ b/actix-router/benches/router.rs @@ -1,6 +1,8 @@ //! Based on https://github.com/ibraheemdev/matchit/blob/master/benches/bench.rs -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, Criterion}; macro_rules! register { (colon) => {{ diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index cd1ad4c66..c2f7dbb31 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -513,20 +513,17 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream { Err(err) => return input_and_compile_error(input, err), }; - let (methods, others) = ast - .attrs - .into_iter() - .map(|attr| match MethodType::from_path(attr.path()) { - Ok(method) => Ok((method, attr)), - Err(_) => Err(attr), - }) - .partition::, _>(Result::is_ok); + let mut methods = Vec::new(); - ast.attrs = others.into_iter().map(Result::unwrap_err).collect(); + for attr in std::mem::take(&mut ast.attrs) { + match MethodType::from_path(attr.path()) { + Ok(method) => methods.push((method, attr)), + Err(_) => ast.attrs.push(attr), + } + } let methods = match methods .into_iter() - .map(Result::unwrap) .map(|(method, attr)| { attr.parse_args() .and_then(|args| Args::new(args, Some(method))) diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index e0dffae8e..e3b31532b 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -7,13 +7,16 @@ - Panic when calling `Route::to()` or `Route::service()` after `Route::wrap()` to prevent silently dropping route middleware. [#3944] - Fix `HttpRequest::{match_pattern,match_name}` reporting path-only matches when route guards disambiguate overlapping resources. [#3346] - Fix `Readlines` handling of lines split across payload chunks so combined line limits are enforced and complete lines are yielded. +- Fix app data being retained after graceful shutdown with in-flight slow request bodies. [#3100] - Update `foldhash` dependency to `0.2`. - Update `rand` dependency to `0.10`. +- Update `impl-more` dependency to `0.3`. - Add `HttpServer::h1_write_buffer_size()`. [#3944]: https://github.com/actix/actix-web/pull/3944 [#3346]: https://github.com/actix/actix-web/issues/3346 [#3542]: https://github.com/actix/actix-web/issues/3542 +[#3100]: https://github.com/actix/actix-web/issues/3100 ## 4.13.0 diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index f6e05d325..288e78d08 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -150,7 +150,7 @@ encoding_rs = "0.8" foldhash = "0.2" futures-core = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false } -impl-more = "0.1.4" +impl-more = "0.3.1" itoa = "1" language-tags = "0.3" log = "0.4" @@ -176,7 +176,7 @@ awc = { version = "3", features = ["openssl"] } brotli = "8" const-str = "1.1" core_affinity = "0.8" -criterion = { version = "0.5", features = ["html_reports"] } +criterion = { version = "0.8", features = ["html_reports"] } env_logger = "0.11" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false, features = ["std"] } diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index fadcf825b..1f99d2095 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -255,7 +255,7 @@ where T: Service, Error = Error>, { fn drop(&mut self) { - self.app_state.pool().clear(); + self.app_state.pool().disable(); } } diff --git a/actix-web/src/request.rs b/actix-web/src/request.rs index 08be38ca6..d4430bd51 100644 --- a/actix-web/src/request.rs +++ b/actix-web/src/request.rs @@ -1,5 +1,5 @@ use std::{ - cell::{Ref, RefCell, RefMut}, + cell::{Cell, Ref, RefCell, RefMut}, collections::HashMap, fmt, hash::{BuildHasher, Hash}, @@ -669,6 +669,7 @@ impl fmt::Debug for HttpRequest { /// The pool's default capacity is 128 items. pub(crate) struct HttpRequestPool { inner: RefCell>>, + enabled: Cell, cap: usize, } @@ -682,6 +683,7 @@ impl HttpRequestPool { pub(crate) fn with_capacity(cap: usize) -> Self { HttpRequestPool { inner: RefCell::new(Vec::with_capacity(cap)), + enabled: Cell::new(true), cap, } } @@ -698,7 +700,7 @@ impl HttpRequestPool { /// Check if the pool still has capacity for request storage. #[inline] pub(crate) fn is_available(&self) -> bool { - self.inner.borrow_mut().len() < self.cap + self.enabled.get() && self.inner.borrow().len() < self.cap } /// Push a request to pool. @@ -707,15 +709,16 @@ impl HttpRequestPool { self.inner.borrow_mut().push(req); } - /// Clears all allocated HttpRequest objects. - pub(crate) fn clear(&self) { - self.inner.borrow_mut().clear() + /// Prevents future requests from being returned to the pool and clears existing entries. + pub(crate) fn disable(&self) { + self.enabled.set(false); + self.inner.borrow_mut().clear(); } } #[cfg(test)] mod tests { - use std::collections::HashMap; + use std::{collections::HashMap, sync::Arc}; use bytes::Bytes; @@ -993,6 +996,41 @@ mod tests { assert_eq!(resp.headers().get("pool_cap").unwrap(), "128"); } + #[actix_rt::test] + async fn test_request_dropped_after_service_does_not_reenter_pool() { + struct State { + _data: Arc, + } + + let (weak_data, app_data) = { + let data = Arc::new("data".to_owned()); + (Arc::downgrade(&data), web::Data::new(State { _data: data })) + }; + + let held_req = Rc::new(RefCell::new(None)); + + { + let held_req = Rc::clone(&held_req); + let srv = init_service(App::new().app_data(app_data).service(web::resource("/").to( + move |req: HttpRequest| { + *held_req.borrow_mut() = Some(req.clone()); + HttpResponse::Ok() + }, + ))) + .await; + + let resp = call_service(&srv, TestRequest::default().to_request()).await; + assert_eq!(resp.status(), StatusCode::OK); + + drop(resp); + drop(srv); + } + + assert!(weak_data.upgrade().is_some()); + drop(held_req.borrow_mut().take()); + assert!(weak_data.upgrade().is_none()); + } + #[actix_rt::test] async fn test_data() { let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to( diff --git a/actix-web/tests/test_httpserver.rs b/actix-web/tests/test_httpserver.rs index 44283ebd4..403b66d83 100644 --- a/actix-web/tests/test_httpserver.rs +++ b/actix-web/tests/test_httpserver.rs @@ -1,9 +1,16 @@ #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; -use std::{sync::mpsc, thread, time::Duration}; +use std::{ + convert::Infallible, + sync::{mpsc, Arc}, + thread, + time::Duration, +}; -use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; +use actix_web::{rt::time::sleep, web, App, HttpRequest, HttpResponse, HttpServer}; +use bytes::Bytes; +use futures_util::stream; #[actix_rt::test] async fn test_start() { @@ -74,6 +81,74 @@ async fn test_start() { srv.stop(false).await; } +#[actix_rt::test] +async fn test_app_data_dropped_after_graceful_shutdown_with_slow_request() { + struct State { + _data: Arc, + } + + async fn echo(_body: web::Json) -> HttpResponse { + HttpResponse::Ok().finish() + } + + let (weak_data, app_data) = { + let data = Arc::new("data".to_owned()); + (Arc::downgrade(&data), web::Data::new(State { _data: data })) + }; + + let server = HttpServer::new(move || { + App::new() + .app_data(app_data.clone()) + .service(web::resource("/echo").route(web::post().to(echo))) + }) + .workers(1) + .shutdown_timeout(1) + .bind(("127.0.0.1", 0)) + .unwrap(); + + let addr = server.addrs()[0]; + let server = server.run(); + let server_handle = server.handle(); + + let send_request = async move { + sleep(Duration::from_millis(100)).await; + + let slow_body = stream::unfold(0, |idx| async move { + if idx < 8 { + sleep(Duration::from_millis(200)).await; + Some((Ok::<_, Infallible>(Bytes::from_static(b" ")), idx + 1)) + } else { + None + } + }); + + let client = awc::Client::default(); + let _ = client + .post(format!("http://{addr}/echo")) + .insert_header(("content-type", "application/json")) + .send_stream(slow_body) + .await; + }; + + let graceful_stop = async move { + sleep(Duration::from_millis(300)).await; + server_handle.stop(true).await; + }; + + let (server_res, (), ()) = tokio::join!(server, send_request, graceful_stop); + server_res.unwrap(); + + for _ in 0..20 { + sleep(Duration::from_millis(100)).await; + + if weak_data.upgrade().is_none() { + return; + } + } + + panic!("app data still referenced after graceful shutdown"); +} + #[cfg(feature = "openssl")] fn ssl_acceptor() -> openssl::ssl::SslAcceptorBuilder { use openssl::{ diff --git a/awc/CHANGES.md b/awc/CHANGES.md index e523c0fc7..9888c66c3 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - Add camel-case header controls to `WebsocketsRequest` via `camel_case_headers()` and `set_camel_case_headers()`. [#3953] +- Update `hickory-resolver` dependency to `0.26.1`. - Update `rand` dependency to `0.10`. [#3953]: https://github.com/actix/actix-web/pull/3953 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 66bf24d9c..a980f705b 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -131,7 +131,7 @@ tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true, featu tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true } tls-rustls-0_23 = { package = "rustls", version = "0.23", optional = true, default-features = false } -hickory-resolver = { version = "0.25", optional = true, features = ["system-config", "tokio"] } +hickory-resolver = { version = "0.26.1", optional = true, features = ["system-config", "tokio"] } [dev-dependencies] actix-http = { version = "3.12", features = ["openssl"] } diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs index 0ce542365..4297b00d1 100644 --- a/awc/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -1053,7 +1053,7 @@ mod resolver { use actix_tls::connect::Resolve; use hickory_resolver::{ config::{ResolverConfig, ResolverOpts}, - name_server::TokioConnectionProvider, + net::runtime::TokioRuntimeProvider, system_conf::read_system_conf, TokioResolver, }; @@ -1102,9 +1102,10 @@ mod resolver { }; let resolver = - TokioResolver::builder_with_config(cfg, TokioConnectionProvider::default()) + TokioResolver::builder_with_config(cfg, TokioRuntimeProvider::default()) .with_options(opts) - .build(); + .build() + .expect("failed to build Hickory DNS resolver"); Resolver::custom(HickoryDnsResolver(resolver)) }) diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs index 95d4c15f1..85fb756f4 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -2,16 +2,24 @@ extern crate tls_openssl as openssl; -use std::sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, +use std::{ + convert::Infallible, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, + }, }; -use actix_http::HttpService; +use actix_http::{HttpService, Request, Response}; use actix_http_test::test_server; use actix_service::{fn_service, map_config, ServiceFactoryExt}; use actix_utils::future::ok; -use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; +use actix_web::{ + dev::AppConfig, + http::{header, Version}, + web, App, HttpResponse, +}; +use futures_util::stream; use openssl::{ pkey::PKey, ssl::{SslAcceptor, SslConnector, SslMethod, SslVerifyMode}, @@ -92,3 +100,55 @@ async fn test_connection_reuse_h2() { // one connection assert_eq!(num.load(Ordering::Relaxed), 1); } + +// Regression test for https://github.com/actix/actix-web/issues/2305. +#[actix_rt::test] +async fn h2_streaming_body_does_not_send_transfer_encoding() { + let has_transfer_encoding = Arc::new(AtomicBool::new(false)); + let has_transfer_encoding2 = Arc::clone(&has_transfer_encoding); + + let srv = test_server(move || { + let has_transfer_encoding = Arc::clone(&has_transfer_encoding2); + + HttpService::build() + .h2(move |req: Request| { + let has_transfer_encoding = Arc::clone(&has_transfer_encoding); + + async move { + has_transfer_encoding.store( + req.head().headers.contains_key(header::TRANSFER_ENCODING), + Ordering::Relaxed, + ); + + Ok::<_, Infallible>(Response::ok()) + } + }) + .openssl(tls_config()) + .map_err(|_| ()) + }) + .await; + + // disable ssl verification + 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)); + + let client = awc::Client::builder() + .connector(awc::Connector::new().openssl(builder.build())) + .finish(); + + let response = client + .post(srv.surl("/")) + .version(Version::HTTP_2) + .send_stream(stream::once(async { + Ok::<_, Infallible>(bytes::Bytes::from_static(b"hello")) + })) + .await + .unwrap(); + + assert!(response.status().is_success()); + assert_eq!(response.version(), Version::HTTP_2); + assert!(!has_transfer_encoding.load(Ordering::Relaxed)); +} diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 000000000..4f9044a27 --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,8 @@ +rules: + dangerous-triggers: + ignore: + # Required for labeling PRs from forks; does not check out PR head. + - labeler.yml:3 + dependabot-cooldown: + config: + days: 3