dav-server-0.11.0/.cargo_vcs_info.json0000644000000001361046102023000132130ustar { "git": { "sha1": "55facb913b35aeaa1d4a73b4453380baeb85804c" }, "path_in_vcs": "" }dav-server-0.11.0/.github/workflows/CI.yml000064400000000000000000000055471046102023000164070ustar 00000000000000on: [push, pull_request] name: CI jobs: check: name: Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: actions-rs/cargo@v1 with: command: check args: --all-features test: name: Test Suite runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: Swatinem/rust-cache@v1 - name: Check build with --no-default-features uses: actions-rs/cargo@v1 with: command: build args: --no-default-features - name: Check build with default features uses: actions-rs/cargo@v1 with: command: build - name: Check build with warp uses: actions-rs/cargo@v1 with: command: build args: --features warp-compat - name: Check build with actix-web uses: actions-rs/cargo@v1 with: command: build args: --features actix-compat - name: Check build with caldav uses: actions-rs/cargo@v1 with: command: build args: --features caldav - name: Test uses: actions-rs/cargo@v1 with: command: test args: --all-features --all compliance: name: Compliance Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: Swatinem/rust-cache@v1 - name: Build sample litmus server run: cargo build --example sample-litmus-server - name: Run sample litmus server run: | cargo run --example sample-litmus-server -- --memfs --auth & sleep 5 - name: Build litmus run: | curl -O http://www.webdav.org/neon/litmus/litmus-0.13.tar.gz tar xf litmus-0.13.tar.gz cd litmus-0.13 ./configure make - name: Run litmus protocol compliance test run: | cd litmus-0.13 TESTS="http basic copymove locks props" HTDOCS=htdocs TESTROOT=. ./litmus http://localhost:4918/ someuser somepass fmt: name: Rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check dav-server-0.11.0/.github/workflows/rust.yml000064400000000000000000000025711046102023000171030ustar 00000000000000name: Push or PR on: [push, pull_request] env: CARGO_TERM_COLOR: always jobs: build_n_test: strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: rustfmt if: ${{ !cancelled() }} run: cargo fmt --all -- --check - name: check if: ${{ !cancelled() }} run: cargo check --verbose - name: clippy if: ${{ !cancelled() }} run: | cargo clippy --all-targets -- -D warnings cargo clippy --all-targets -- -D warnings cargo clippy --all-targets -- -D warnings - name: Build if: ${{ !cancelled() }} run: | cargo build --verbose --examples --tests cargo build --verbose --examples --tests cargo build --verbose --examples --tests - name: Abort on error if: ${{ failure() }} run: echo "Some of jobs failed" && false semver: name: Check semver strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - uses: obi1kenobi/cargo-semver-checks-action@v2dav-server-0.11.0/.gitignore000064400000000000000000000000641046102023000137510ustar 00000000000000Cargo.lock /target/ **/*.rs.bk .vscode src/xml .ideadav-server-0.11.0/Cargo.lock0000644000001712411046102023000111740ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "actix-codec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ "bitflags", "bytes", "futures-core", "futures-sink", "memchr", "pin-project-lite", "tokio", "tokio-util", "tracing", ] [[package]] name = "actix-http" version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa882656b67966045e4152c634051e70346939fced7117d5f0b52146a7c74c9" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", "base64 0.22.1", "bitflags", "bytes", "bytestring", "derive_more", "encoding_rs", "foldhash 0.1.5", "futures-core", "http 0.2.12", "httparse", "httpdate", "itoa", "language-tags", "local-channel", "mime", "percent-encoding", "pin-project-lite", "rand", "sha1", "smallvec", "tokio", "tokio-util", "tracing", ] [[package]] name = "actix-macros" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", "syn", ] [[package]] name = "actix-router" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if", "http 0.2.12", "regex-lite", "serde", "tracing", ] [[package]] name = "actix-rt" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" dependencies = [ "futures-core", "tokio", ] [[package]] name = "actix-server" version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "futures-util", "mio", "socket2", "tokio", "tracing", ] [[package]] name = "actix-service" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "actix-utils" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" dependencies = [ "local-waker", "pin-project-lite", ] [[package]] name = "actix-web" version = "4.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2e3b15b3dc6c6ed996e4032389e9849d4ab002b1e92fbfe85b5f307d1479b4d" dependencies = [ "actix-codec", "actix-http", "actix-macros", "actix-router", "actix-rt", "actix-server", "actix-service", "actix-utils", "actix-web-codegen", "bytes", "bytestring", "cfg-if", "derive_more", "encoding_rs", "foldhash 0.1.5", "futures-core", "futures-util", "impl-more", "itoa", "language-tags", "log", "mime", "once_cell", "pin-project-lite", "regex-lite", "serde", "serde_json", "serde_urlencoded", "smallvec", "socket2", "time", "tracing", "url", ] [[package]] name = "actix-web-codegen" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" dependencies = [ "actix-router", "proc-macro2", "quote", "syn", ] [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", "windows-sys 0.59.0", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", "bytes", "form_urlencoded", "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", "hyper-util", "itoa", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sync_wrapper", "tokio", "tower", "tower-layer", "tower-service", "tracing", ] [[package]] name = "axum-core" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", "http 1.3.1", "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", "sync_wrapper", "tower-layer", "tower-service", "tracing", ] [[package]] name = "backtrace" version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets", ] [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytestring" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" dependencies = [ "bytes", ] [[package]] name = "calcard" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64bb5cc4aa43df0df11be8fee3f375ce286876cb859ff6f58173d1619858f550" dependencies = [ "ahash", "chrono", "chrono-tz", "hashify", "mail-builder", "mail-parser", ] [[package]] name = "cc" version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link 0.2.1", ] [[package]] name = "chrono-tz" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" dependencies = [ "chrono", "phf", ] [[package]] name = "clap" version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "dav-server" version = "0.11.0" dependencies = [ "actix-web", "axum", "bytes", "calcard", "chrono", "clap", "derive-where", "dyn-clone", "env_logger", "futures-channel", "futures-util", "headers 0.4.0", "htmlescape", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", "hyper-util", "icalendar", "libc", "log", "lru", "mime_guess", "parking_lot", "percent-encoding", "pin-project-lite", "reflink-copy", "tokio", "url", "uuid", "warp", "xml-rs", "xmltree", ] [[package]] name = "deranged" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] [[package]] name = "derive-where" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "derive_more" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", "syn", "unicode-xid", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dyn-clone" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-sink", "futures-task", "pin-project-lite", "pin-utils", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", ] [[package]] name = "hashify" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "149e3ea90eb5a26ad354cfe3cb7f7401b9329032d0235f2687d03a35f30e5d4c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "headers" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ "base64 0.21.7", "bytes", "headers-core 0.2.0", "http 0.2.12", "httpdate", "mime", "sha1", ] [[package]] name = "headers" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.7", "bytes", "headers-core 0.3.0", "http 1.3.1", "httpdate", "mime", "sha1", ] [[package]] name = "headers-core" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ "http 0.2.12", ] [[package]] name = "headers-core" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ "http 1.3.1", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "htmlescape" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http 0.2.12", "pin-project-lite", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.3.1", ] [[package]] name = "http-body-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", "http 1.3.1", "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", "want", ] [[package]] name = "hyper" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", "pin-utils", "smallvec", "tokio", ] [[package]] name = "hyper-util" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "bytes", "futures-core", "http 1.3.1", "http-body 1.0.1", "hyper 1.8.1", "pin-project-lite", "tokio", "tower-service", ] [[package]] name = "iana-time-zone" version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icalendar" version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc3b69b799a03e059f6dc984c25a8bf847d8ca4cbddb079c39ede7b3d24854c3" dependencies = [ "chrono", "iso8601", "nom", "nom-language", "uuid", ] [[package]] name = "icu_collections" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_locid_transform" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_locid_transform_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "utf16_iter", "utf8_iter", "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", "icu_locid_transform", "icu_properties_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_provider_macros" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "impl-more" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "iso8601" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" dependencies = [ "nom", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde", ] [[package]] name = "jiff-static" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "js-sys" version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "language-tags" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "local-channel" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" dependencies = [ "futures-core", "futures-sink", "local-waker", ] [[package]] name = "local-waker" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ "hashbrown 0.16.1", ] [[package]] name = "mail-builder" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900998f307338c4013a28ab14d760b784067324b164448c6d98a89e44810473b" [[package]] name = "mail-parser" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82a3d6522697593ba4c683e0a6ee5a40fee93bc1a525e3cc6eeb3da11fd8897" dependencies = [ "hashify", ] [[package]] name = "matchit" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", ] [[package]] name = "miniz_oxide" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "nom" version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" dependencies = [ "memchr", ] [[package]] name = "nom-language" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29" dependencies = [ "nom", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" dependencies = [ "phf_shared", ] [[package]] name = "phf_shared" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" dependencies = [ "siphasher", ] [[package]] name = "pin-project" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "portable-atomic" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", "rand_core", ] [[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", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags", ] [[package]] name = "reflink-copy" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c81d000a2c524133cc00d2f92f019d399e57906c3b7119271a2495354fe895" dependencies = [ "cfg-if", "libc", "rustix", "windows", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-lite" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", "serde_core", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] [[package]] name = "siphasher" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "synstructure" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "time" version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tokio" version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-util" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "tower" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicase" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf16_iter" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ "getrandom", "js-sys", "wasm-bindgen", ] [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "warp" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" dependencies = [ "bytes", "futures-channel", "futures-util", "headers 0.3.9", "http 0.2.12", "hyper 0.14.32", "log", "mime", "mime_guess", "percent-encoding", "pin-project", "scoped-tls", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-util", "tower-service", "tracing", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "windows" version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ "windows-collections", "windows-core", "windows-future", "windows-link 0.1.1", "windows-numerics", ] [[package]] name = "windows-collections" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ "windows-core", ] [[package]] name = "windows-core" version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.1", "windows-result", "windows-strings", ] [[package]] name = "windows-future" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ "windows-core", "windows-link 0.1.1", ] [[package]] name = "windows-implement" version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", "windows-link 0.1.1", ] [[package]] name = "windows-result" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ "windows-link 0.1.1", ] [[package]] name = "windows-strings" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ "windows-link 0.1.1", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "xml" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" [[package]] name = "xml-rs" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3a56132a0d6ecbe77352edc10232f788fc4ceefefff4cab784a98e0e16b6b51" dependencies = [ "xml", ] [[package]] name = "xmltree" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc04313cab124e498ab1724e739720807b6dc405b9ed0edc5860164d2e4ff70" dependencies = [ "xml", ] [[package]] name = "yoke" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerocopy" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", "syn", ] dav-server-0.11.0/Cargo.toml0000644000000105041046102023000112110ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" name = "dav-server" version = "0.11.0" authors = [ "Miquel van Smoorenburg ", "messense ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Rust WebDAV server library. A fork of the webdav-handler crate." readme = "README.md" keywords = ["webdav"] categories = ["web-programming"] license = "Apache-2.0" repository = "https://github.com/messense/dav-server-rs" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [package.metadata.playground] features = ["full"] [features] actix-compat = ["actix-web"] all = [ "actix-compat", "warp-compat", "caldav", "carddav", ] caldav = ["icalendar"] carddav = ["calcard"] default = [ "localfs", "memfs", ] localfs = [ "libc", "lru", "tokio/rt-multi-thread", "parking_lot", "reflink-copy", ] memfs = ["libc"] warp-compat = [ "warp", "hyper", ] [lib] name = "dav_server" path = "src/lib.rs" [[example]] name = "actix" path = "examples/actix.rs" required-features = ["actix-compat"] [[example]] name = "auth" path = "examples/auth.rs" [[example]] name = "axum" path = "examples/axum.rs" [[example]] name = "caldav" path = "examples/caldav.rs" required-features = ["caldav"] [[example]] name = "carddav" path = "examples/carddav.rs" required-features = ["carddav"] [[example]] name = "hyper" path = "examples/hyper.rs" [[example]] name = "sample-litmus-server" path = "examples/sample-litmus-server.rs" [[example]] name = "warp" path = "examples/warp.rs" required-features = ["warp-compat"] [[test]] name = "caldav_tests" path = "tests/caldav_tests.rs" [[test]] name = "carddav_tests" path = "tests/carddav_tests.rs" [dependencies.actix-web] version = "4.0.0-beta.15" optional = true default-features = false [dependencies.bytes] version = "1.0.1" [dependencies.calcard] version = "0.3" optional = true default-features = false [dependencies.chrono] version = "0.4" features = ["clock"] default-features = false [dependencies.derive-where] version = "1.6.0" [dependencies.dyn-clone] version = "1" [dependencies.futures-channel] version = "0.3.16" [dependencies.futures-util] version = "0.3.16" features = ["alloc"] default-features = false [dependencies.headers] version = "0.4.0" [dependencies.htmlescape] version = "0.3.1" [dependencies.http] version = "1.0.0" [dependencies.http-body] version = "1.0.0" [dependencies.http-body-util] version = "0.1.0" [dependencies.hyper] version = "1.1.0" features = ["server"] optional = true default-features = false [dependencies.icalendar] version = "0.17.1" optional = true [dependencies.libc] version = "0.2.0" optional = true [dependencies.log] version = "0.4.0" [dependencies.lru] version = "0.16.0" optional = true [dependencies.mime_guess] version = "2.0.0" [dependencies.parking_lot] version = "0.12.0" optional = true [dependencies.percent-encoding] version = "2.1.0" [dependencies.pin-project-lite] version = "0.2.16" [dependencies.reflink-copy] version = "0.1.14" optional = true [dependencies.tokio] version = "1.22.0" features = ["rt"] [dependencies.url] version = "2.2.0" [dependencies.uuid] version = "1.1.2" features = ["v4"] [dependencies.warp] version = "0.3.0" optional = true default-features = false [dependencies.xml-rs] version = "1" [dependencies.xmltree] version = "0.12.0" [dev-dependencies.actix-web] version = "4.0.0-beta.15" features = ["macros"] default-features = false [dev-dependencies.axum] version = "0.8" features = [] [dev-dependencies.clap] version = "4.0.0" features = ["derive"] [dev-dependencies.env_logger] version = "0.11.0" [dev-dependencies.hyper] version = "1.1.0" features = [ "http1", "server", ] [dev-dependencies.hyper-util] version = "0.1.19" features = ["tokio"] [dev-dependencies.tokio] version = "1.3.0" features = ["full"] dav-server-0.11.0/Cargo.toml.orig000064400000000000000000000051661046102023000146600ustar 00000000000000[package] name = "dav-server" version = "0.11.0" readme = "README.md" description = "Rust WebDAV server library. A fork of the webdav-handler crate." repository = "https://github.com/messense/dav-server-rs" authors = ["Miquel van Smoorenburg ", "messense "] edition = "2024" license = "Apache-2.0" keywords = ["webdav"] categories = ["web-programming"] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [package.metadata.playground] features = ["full"] [features] default = ["localfs", "memfs"] actix-compat = [ "actix-web" ] warp-compat = [ "warp", "hyper" ] caldav = [ "icalendar" ] carddav = [ "calcard" ] all = [ "actix-compat", "warp-compat", "caldav", "carddav" ] localfs = ["libc", "lru", "tokio/rt-multi-thread", "parking_lot", "reflink-copy"] memfs = ["libc"] [[example]] name = "actix" required-features = [ "actix-compat" ] [[example]] name = "warp" required-features = [ "warp-compat" ] [[example]] name = "caldav" required-features = [ "caldav" ] [[example]] name = "carddav" required-features = [ "carddav" ] [dependencies] bytes = "1.0.1" dyn-clone = "1" futures-util = { version = "0.3.16", default-features = false, features = ["alloc"] } futures-channel = "0.3.16" headers = "0.4.0" htmlescape = "0.3.1" http = "1.0.0" http-body = "1.0.0" http-body-util = "0.1.0" libc = { version = "0.2.0", optional = true } log = "0.4.0" lru = { version = "0.16.0", optional = true } mime_guess = "2.0.0" parking_lot = { version = "0.12.0", optional = true } percent-encoding = "2.1.0" pin-project-lite = "0.2.16" tokio = { version = "1.22.0", features = [ "rt" ] } chrono = { version = "0.4", default-features = false, features = [ "clock" ] } url = "2.2.0" uuid = { version = "1.1.2", features = ["v4"] } xml-rs = "1" xmltree = "0.12.0" hyper = { version = "1.1.0", default-features = false, features = ["server"], optional = true } warp = { version = "0.3.0", optional = true, default-features = false } actix-web = { version = "4.0.0-beta.15", default-features = false, optional = true } reflink-copy = { version = "0.1.14", optional = true } icalendar = { version = "0.17.1", optional = true } calcard = { version = "0.3", default-features = false, optional = true } derive-where = "1.6.0" [dev-dependencies] clap = { version = "4.0.0", features = ["derive"] } env_logger = "0.11.0" actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["macros"] } hyper = { version = "1.1.0", features = ["http1", "server"] } hyper-util = { version = "0.1.19", features = ["tokio"] } tokio = { version = "1.3.0", features = ["full"] } axum = { version = "0.8", features = [] }dav-server-0.11.0/README.CalDAV.md000064400000000000000000000156411046102023000143000ustar 00000000000000# CalDAV Support in dav-server This document describes the CalDAV (Calendaring Extensions to WebDAV) support in the dav-server library. ## Overview CalDAV is an extension of WebDAV that provides a standard way to access and manage calendar data over HTTP. It's defined in [RFC 4791](https://tools.ietf.org/html/rfc4791) and allows calendar clients to: - Create and manage calendar collections - Store and retrieve calendar events, tasks, and journals - Query calendars with complex filters - Synchronize calendar data between clients and servers ## Features The CalDAV implementation in dav-server includes: - **Calendar Collections**: A directory that functions as a calendar, containing one `.ics` file for each event. - **MKCALENDAR Method**: Create new calendar collection - **REPORT Method**: Query calendar data with filters - **CalDAV Properties**: Calendar-specific WebDAV properties - **iCalendar Support**: Parse and validate iCalendar data - **Time Range Queries**: Filter events by date/time ranges - **Component Filtering**: Filter by calendar component types (VEVENT, VTODO, etc.) ## Enabling CalDAV CalDAV support is available as an optional cargo feature: ```toml [dependencies] dav-server = { version = "0.8", features = ["caldav"] } ``` ## Quick Start Here's a basic CalDAV server setup: ```rust use dav_server::{DavHandler, fakels::FakeLs, localfs::LocalFs}; let server = DavHandler::builder() .filesystem(LocalFs::new("/dav_files", false, false, false)) .locksystem(FakeLs::new()) .build_handler(); ``` ## Important Setup Notes **CalDAV Directory Creation**: The `/calendars` directory (defined in `dav_server::caldav::DEFAULT_CALDAV_DIRECTORY`) must exist before CalDAV operations. `MemFs` and `LocalFs` create it automatically, but custom `GuardedFileSystem` implementations must initialize it during startup. ## CalDAV Methods ### MKCALENDAR Creates a new calendar collection: ```bash curl -X MKCALENDAR http://localhost:8080/calendars/my-calendar/ ``` With properties: ```bash curl -X MKCALENDAR http://localhost:8080/calendars/my-calendar/ \ -H "Content-Type: application/xml" \ --data ' My Calendar Personal calendar ' ``` ### REPORT Query calendar data: #### Calendar Query ```bash curl -X REPORT http://localhost:8080/calendars/my-calendar/ \ -H "Content-Type: application/xml" \ -H "Depth: 1" \ --data ' ' ``` #### Calendar Multiget ```bash curl -X REPORT http://localhost:8080/calendars/my-calendar/ \ -H "Content-Type: application/xml" \ --data ' /my-calendar/event1.ics /my-calendar/event2.ics ' ``` ## CalDAV Properties The implementation supports standard CalDAV properties: ### Collection Properties - `calendar-description`: Human-readable description - `calendar-timezone`: Default timezone for the calendar - `supported-calendar-component-set`: Supported component types (VEVENT, VTODO, etc.) - `supported-calendar-data`: Supported calendar data formats - `max-resource-size`: Maximum size for calendar resources ### Principal Properties - `calendar-home-set`: URL of the user's calendar home collection - `calendar-user-address-set`: Calendar user's addresses - `schedule-inbox-URL`: URL for scheduling messages - `schedule-outbox-URL`: URL for outgoing scheduling ## Working with Calendar Data ### Adding Events Store iCalendar data using PUT: ```bash curl -X PUT http://localhost:8080/calendars/my-calendar/event.ics \ -H "Content-Type: text/calendar" \ --data 'BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp//CalDAV Client//EN BEGIN:VEVENT UID:12345@example.com DTSTART:20240101T120000Z DTEND:20240101T130000Z SUMMARY:New Year Meeting DESCRIPTION:Planning meeting for the new year END:VEVENT END:VCALENDAR' ``` ### Retrieving Events Use GET to retrieve individual calendar resources: ```bash curl http://localhost:8080/calendars/my-calendar/event.ics ``` ## Client Compatibility The CalDAV implementation has been tested with: - **Thunderbird**: Full support for calendar sync - **Apple Calendar**: Compatible with basic operations - **CalDAV-Sync (Android)**: Works with standard CalDAV features - **Evolution**: Support for calendar collections and events ## Limitations Current limitations include: - No scheduling support (iTIP/iMIP) - Limited calendar-user-principal support - No calendar sharing or ACL support - Basic time zone handling - No recurring event expansion in queries ## Example Applications These calendar server examples lacks authentication and does not support user-specific access. The default FileSystems can only create collections on the path "/calendars". For a production environment, you should implement the GuardedFileSystem for better security and user management. ### Calendar Server ```rust use dav_server::{DavHandler, fakels::FakeLs, localfs::LocalFs}; use std::net::SocketAddr; #[tokio::main] async fn main() { let server = DavHandler::builder() .filesystem(LocalFs::new("/calendars", false, false, false)) .locksystem(FakeLs::new()) .build_handler(); // Serve on port 8080 // Calendars accessible at http://localhost:8080/calendars/ } ``` ### Multi-tenant Calendar Service ```rust use dav_server::{DavHandler, memfs::MemFs, memls::MemLs}; // Use in-memory filesystem for demonstration let server = DavHandler::builder() .filesystem(MemFs::new()) .locksystem(MemLs::new()) .principal("/principals/user1/") .build_handler(); ``` ## Testing Run CalDAV tests with: ```bash cargo test --features caldav caldav_tests ``` Run the CalDAV example: ```bash cargo run --example caldav --features caldav ``` ## Standards Compliance This implementation follows: - [RFC 4791](https://tools.ietf.org/html/rfc4791) - Calendaring Extensions to WebDAV (CalDAV) - [RFC 5545](https://tools.ietf.org/html/rfc5545) - Internet Calendaring and Scheduling Core Object Specification (iCalendar) - [RFC 4918](https://tools.ietf.org/html/rfc4918) - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV) ## Contributing Contributions to improve CalDAV support are welcome. Areas for enhancement include: - Scheduling support (iTIP) - Additional client compatibility testingdav-server-0.11.0/README.litmus-test.md000064400000000000000000000115041046102023000155320ustar 00000000000000 # Webdav protocol compliance. The standard for webdav compliance testing is [`litmus`](http://www.webdav.org/neon/litmus/), which is available at [http://www.webdav.org/neon/litmus/](http://www.webdav.org/neon/litmus/). Building it: ``` curl -O http://www.webdav.org/neon/litmus/litmus-0.13.tar.gz tar xf litmus-0.13.tar.gz cd litmus-0.13 ./configure make ``` Then run the test server (`sample-litmus-server`). For some tests, `litmus` assumes that it is using basic authentication, so you must run the server with the `--auth` flag. ``` cd webdav-handler-rs cargo run --example sample-litmus-server -- --memfs --auth ``` You do not have to install the litmus binary, it's possible to run the tests straight from the unpacked & compiled litmus directory (`someuser` and `somepass` are literal, you do not have to put a real username/password there): ``` $ cd litmus-0.13 $ TESTS="http basic copymove locks props" HTDOCS=htdocs TESTROOT=. ./litmus http://localhost:4918/ someuser somepass -> running `http': 0. init.................. pass 1. begin................. pass 2. expect100............. pass 3. finish................ pass <- summary for `http': of 4 tests run: 4 passed, 0 failed. 100.0% -> running `basic': 0. init.................. pass 1. begin................. pass 2. options............... pass 3. put_get............... pass 4. put_get_utf8_segment.. pass 5. put_no_parent......... pass 6. mkcol_over_plain...... pass 7. delete................ pass 8. delete_null........... pass 9. delete_fragment....... WARNING: DELETE removed collection resource with Request-URI including fragment; unsafe ...................... pass (with 1 warning) 10. mkcol................. pass 11. mkcol_again........... pass 12. delete_coll........... pass 13. mkcol_no_parent....... pass 14. mkcol_with_body....... pass 15. finish................ pass <- summary for `basic': of 16 tests run: 16 passed, 0 failed. 100.0% -> 1 warning was issued. -> running `copymove': 0. init.................. pass 1. begin................. pass 2. copy_init............. pass 3. copy_simple........... pass 4. copy_overwrite........ pass 5. copy_nodestcoll....... pass 6. copy_cleanup.......... pass 7. copy_coll............. pass 8. copy_shallow.......... pass 9. move.................. pass 10. move_coll............. pass 11. move_cleanup.......... pass 12. finish................ pass <- summary for `copymove': of 13 tests run: 13 passed, 0 failed. 100.0% -> running `locks': 0. init.................. pass 1. begin................. pass 2. options............... pass 3. precond............... pass 4. init_locks............ pass 5. put................... pass 6. lock_excl............. pass 7. discover.............. pass 8. refresh............... pass 9. notowner_modify....... pass 10. notowner_lock......... pass 11. owner_modify.......... pass 12. notowner_modify....... pass 13. notowner_lock......... pass 14. copy.................. pass 15. cond_put.............. pass 16. fail_cond_put......... pass 17. cond_put_with_not..... pass 18. cond_put_corrupt_token pass 19. complex_cond_put...... pass 20. fail_complex_cond_put. pass 21. unlock................ pass 22. fail_cond_put_unlocked pass 23. lock_shared........... pass 24. notowner_modify....... pass 25. notowner_lock......... pass 26. owner_modify.......... pass 27. double_sharedlock..... pass 28. notowner_modify....... pass 29. notowner_lock......... pass 30. unlock................ pass 31. prep_collection....... pass 32. lock_collection....... pass 33. owner_modify.......... pass 34. notowner_modify....... pass 35. refresh............... pass 36. indirect_refresh...... pass 37. unlock................ pass 38. unmapped_lock......... pass 39. unlock................ pass 40. finish................ pass <- summary for `locks': of 41 tests run: 41 passed, 0 failed. 100.0% -> running `props': 0. init.................. pass 1. begin................. pass 2. propfind_invalid...... pass 3. propfind_invalid2..... pass 4. propfind_d0........... pass 5. propinit.............. pass 6. propset............... pass 7. propget............... pass 8. propextended.......... pass 9. propmove.............. pass 10. propget............... pass 11. propdeletes........... pass 12. propget............... pass 13. propreplace........... pass 14. propget............... pass 15. propnullns............ pass 16. propget............... pass 17. prophighunicode....... pass 18. propget............... pass 19. propremoveset......... pass 20. propget............... pass 21. propsetremove......... pass 22. propget............... pass 23. propvalnspace......... pass 24. propwformed........... pass 25. propinit.............. pass 26. propmanyns............ pass 27. propget............... pass 28. propcleanup........... pass 29. finish................ pass <- summary for `props': of 30 tests run: 30 passed, 0 failed. 100.0% ``` dav-server-0.11.0/README.md000064400000000000000000000170771046102023000132540ustar 00000000000000# dav-server-rs [![Apache-2.0 licensed](https://img.shields.io/badge/license-Apache2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.txt) [![Crates.io](https://img.shields.io/crates/v/dav-server.svg)](https://crates.io/crates/dav-server) [![docs.rs](https://docs.rs/dav-server/badge.svg)](https://docs.rs/dav-server) A fork of the [webdav-handler-rs](https://github.com/miquels/webdav-handler-rs) project. ### Generic async HTTP/Webdav handler [`Webdav`] (RFC4918) is defined as HTTP (GET/HEAD/PUT/DELETE) plus a bunch of extension methods (PROPFIND, etc). These extension methods are used to manage collections (like unix directories), get information on collections (like unix `ls` or `readdir`), rename and copy items, lock/unlock items, etc. A `handler` is a piece of code that takes a `http::Request`, processes it in some way, and then generates a `http::Response`. This library is a `handler` that maps the HTTP/Webdav protocol to the filesystem. Or actually, "a" filesystem. Included is an adapter for the local filesystem (`localfs`), and an adapter for an in-memory filesystem (`memfs`). So this library can be used as a handler with HTTP servers like [hyper], [warp], [actix-web], etc. Either as a correct and complete HTTP handler for files (GET/HEAD) or as a handler for the entire Webdav protocol. In the latter case, you can mount it as a remote filesystem: Linux, Windows, macOS can all mount Webdav filesystems. ### Backend interfaces. The backend interfaces are similar to the ones from the Go `x/net/webdav package`: - the library contains a [HTTP handler][DavHandler]. - you supply a [filesystem][DavFileSystem] for backend storage, which can optionally implement reading/writing [DAV properties][DavProp]. If the file system requires authorization, implement a [special trait][GuardedFileSystem]. - you can supply a [locksystem][DavLockSystem] that handles webdav locks. The handler in this library works with the standard http types from the `http` and `http_body` crates. That means that you can use it straight away with http libraries / frameworks that also work with those types, like hyper. Compatibility modules for [actix-web][actix-compat] and [warp][warp-compat] are also provided. ### Implemented standards. Currently [passes the "basic", "copymove", "props", "locks" and "http" checks][README_litmus] of the Webdav Litmus Test testsuite. That's all of the base [RFC4918] webdav specification. The litmus test suite also has tests for RFC3744 "acl" and "principal", RFC5842 "bind", and RFC3253 "versioning". Those we do not support right now. The relevant parts of the HTTP RFCs are also implemented, such as the preconditions (If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since, If-Range), partial transfers (Range). Also implemented is `partial PUT`, for which there are currently two non-standard ways to do it: [`PUT` with the `Content-Range` header][PUT], which is what Apache's `mod_dav` implements, and [`PATCH` with the `X-Update-Range` header][PATCH] from `SabreDav`. ### Backends. Included are two filesystems: - [`LocalFs`]: serves a directory on the local filesystem - [`MemFs`]: ephemeral in-memory filesystem. supports DAV properties. You're able to implement custom filesystem adapter: - [`DavFileSystem`][DavFileSystem]: without authorization. - [`GuardedFileSystem`][GuardedFileSystem]: when access control is required. Also included are two locksystems: - [`MemLs`]: ephemeral in-memory locksystem. - [`FakeLs`]: fake locksystem. just enough LOCK/UNLOCK support for macOS/Windows. External filesystems: - [`OpendalFs`](https://github.com/apache/opendal/tree/main/integrations/dav-server): serves different storage services via [opendal](https://github.com/apache/opendal) ### Example. Example server using [hyper] that serves the /tmp directory in r/w mode. You should be able to mount this network share from Linux, macOS and Windows. [Examples][examples] for other frameworks are also available. ```rust use std::{convert::Infallible, net::SocketAddr}; use hyper::{server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; use dav_server::{fakels::FakeLs, localfs::LocalFs, DavHandler}; #[tokio::main] async fn main() { let dir = "/tmp"; let addr: SocketAddr = ([127, 0, 0, 1], 4918).into(); let dav_server = DavHandler::builder() .filesystem(LocalFs::new(dir, false, false, false)) .locksystem(FakeLs::new()) .build_handler(); let listener = TcpListener::bind(addr).await.unwrap(); println!("Listening {addr}"); // We start a loop to continuously accept incoming connections loop { let (stream, _) = listener.accept().await.unwrap(); let dav_server = dav_server.clone(); // Use an adapter to access something implementing `tokio::io` traits as if they implement // `hyper::rt` IO traits. let io = TokioIo::new(stream); // Spawn a tokio task to serve multiple connections concurrently tokio::task::spawn(async move { // Finally, we bind the incoming connection to our `hello` service if let Err(err) = http1::Builder::new() // `service_fn` converts our function in a `Service` .serve_connection( io, service_fn({ move |req| { let dav_server = dav_server.clone(); async move { Ok::<_, Infallible>(dav_server.handle(req).await) } } }), ) .await { eprintln!("Failed serving: {err:?}"); } }); } } ``` [DavHandler]: https://docs.rs/dav-server/latest/dav_server/struct.DavHandler.html [DavFileSystem]: https://docs.rs/dav-server/latest/dav_server/fs/index.html [GuardedFileSystem]: https://docs.rs/dav-server/latest/dav_server/fs/trait.GuardedFileSystem.html [DavLockSystem]: https://docs.rs/dav-server/latest/dav_server/ls/index.html [DavProp]: https://docs.rs/dav-server/latest/dav_server/fs/struct.DavProp.html [`WebDav`]: https://tools.ietf.org/html/rfc4918 [RFC4918]: https://tools.ietf.org/html/rfc4918 [`MemLs`]: https://docs.rs/dav-server/latest/dav_server/memls/index.html [`MemFs`]: https://docs.rs/dav-server/latest/dav_server/memfs/index.html [`LocalFs`]: https://docs.rs/dav-server/latest/dav_server/localfs/index.html [`FakeLs`]: https://docs.rs/dav-server/latest/dav_server/fakels/index.html [actix-compat]: https://docs.rs/dav-server/latest/dav_server/actix/index.html [warp-compat]: https://docs.rs/dav-server/latest/dav_server/warp/index.html [README_litmus]: https://github.com/messense/dav-server-rs/blob/main/README.litmus-test.md [examples]: https://github.com/messense/dav-server-rs/tree/main/examples/ [PUT]: https://github.com/messense/dav-server-rs/tree/main/doc/Apache-PUT-with-Content-Range.md [PATCH]: https://github.com/messense/dav-server-rs/tree/main/doc/SABREDAV-partialupdate.md [hyper]: https://hyper.rs/ [warp]: https://crates.io/crates/warp [actix-web]: https://actix.rs/ ### Building. This crate uses std::future::Future and async/await, so it only works with Rust 1.39 and up. ### Testing. ``` RUST_LOG=dav_server=debug cargo run --example sample-litmus-server ``` This will start a server on port 4918, serving an in-memory filesystem. For other options, run `cargo run --example sample-litmus-server -- --help` ### Copyright and License. * © 2018, 2019, 2020 XS4ALL Internet bv * © 2018, 2019, 2020 Miquel van Smoorenburg * © 2021 - 2023 Messense Lv * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) dav-server-0.11.0/TODO.md000064400000000000000000000070061046102023000130530ustar 00000000000000 # TODO list ## Protocol compliance ### Apply all headers The RFC says that for COPY/MOVE/DELETE with Depth: Infinity all headers must be applied to all resources. For example, in RFC4918 9.6.1: ``` Any headers included with DELETE MUST be applied in processing every resource to be deleted. ``` Currently we do not do this- we do apply the If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since, and If headers to the request url, but not recursively. ### Props on symbolic links Should probably disallow that ### In MOVE/DELETE test locks seperately per resource Right now we check if we hold the locks (if any) for the request url, and paths below it for Depth: Infinity requests. If we don't, the entire request fails. We should really check that for every resource to be MOVEd/DELETEd seperately and only fail those resources. This does mean that we cannot MOVE a collection by doing a simple rename, we must do it resource-per-resource, like COPY. ## Race conditions During long-running requests like MOVE/COPY/DELETE we should really LOCK the resource so that no other request can race us. Actually, check if this is true. Isn't the webdav client responsible for this? Anyway: - if the resource is locked exclusively and we hold the lock- great, nothing to do - otherwise: - lock the request URL exclusively (unless already locked exclusively), Depth: infinite, _without checking if any other locks already exist_. This is a temporary lock. - now check if we actually can lock the request URL and paths below - if not, unlock, error - go ahead and do the work - unlock The temporary lock should probably have a timeout of say 10 seconds, where we refresh it every 5 seconds or so, so that a stale lock doesn't hang around too long if something goes catastrophically wrong. Might only happen when the lock database is seperate from the webdav server. ## Improvements: - Do fake locking only for user-agents: - /WebDAVFS/ // Apple - /Microsoft Office OneNote 2013/' // MS - /^Microsoft-WebDAV/ // MS this is the list that NextCloud uses for fake locking. probably (WebDAVFS|Microsoft) would do the trick. - API: perhaps move filesystem interface to Path/PathBuf or similar and hide WebPath - add documentation - add tests, tests ... ## Project ideas: - Add support for properties to localfs.rs on XFS. XFS has unlimited and scalable extended attributes. ext2/3/4 can store max 4KB. On XFS we can then also store creationdate in an attribute. - Add support for changing live props like mtime/atime - atime could be done with Win32LastAccessTime - allow setting apache "executable" prop - it appears that there are webdav implementations that allow you to set "DAV:getcontentlength". - we could support (at least return) some Win32FileAttributes: - readonly: 00000001 (unix mode) - hidden: 00000002 (if file starts with a "." - dir: 00000010 - file: 00000020 readonly on dirs means "all files in the directory" so that is best not implemented. - allow setting of some windows live props: - readonly (on files, via chmod) - Win32LastAccessTime, Win32LastModifiedTime - implement [RFC4437 Webdav Redirectref](https://tools.ietf.org/html/rfc4437) -- basically support for symbolic links - implement [RFC3744 Webdac ACL](https://tools.ietf.org/html/rfc3744) ## Things I thought of but aren't going to work: ### Compression - support for compressing responses, at least PROPFIND. - support for compressed PUT requests Nice, but no webdav client that I know of uses compression. dav-server-0.11.0/doc/APPLE-Finder-hints.md000064400000000000000000000032521046102023000162430ustar 00000000000000# APPLE-FINDER-HINTS The Apple Finder (and other subsystems) seem to probe for a few files at the root of the filesystems to get a hint about the behaviour they should show processing this filesystem. It also looks for files with extra localization information in every directory, and for resource fork data (the `._` files). ## FILES - `.metadata_never_index` prevents the system from indexing all of the data - `.ql_disablethumbnails` prevent the system from downloading all files that look like an image or a video to create a thumbnail - `.ql_disablecache` not really sure but it sounds useful The `.ql_` files are configuration for the "QuickLook" functionality of the Finder. The `.metadata_never_index` file appears to be a hint for the Spotlight indexing system. Additionally, the Finder probes for a `.localized` file in every directory it encounters, and it does a PROPSTAT for every file in the directory prefixed with `._`. ## OPTIMIZATIONS For a macOS client we return the metadata for a zero-sized file if it does a PROPSTAT of `/.metadata_never_index` or `/.ql_disablethumbnails`. We always return a 404 Not Found for a PROPSTAT of any `.localized` file. Furthermore, we disallow moving, removing etc of those files. The files do not show up in a PROPSTAT of the rootdirectory. If a PROPFIND with `Depth: 1` is done on a directory, we add the directory pathname to an LRU cache, and the pathname of each file of which the name starts with `._`. Since we then know which `._` files exist, it is easy to return a fast 404 for PROPSTAT request for `._` files that do not exist. The cache is kept consistent by checking the timestamp on the parent directory, and a timeout. dav-server-0.11.0/doc/APPLE-doubleinfo.md000064400000000000000000000044341046102023000160420ustar 00000000000000 # APPLEDOUBLEINFO Normally, after asking for a directory listing (using PROPFIND with Depth: 1) the macOS Finder will send a PROPFIND request for every file in the directory, prefixed with ".\_". Even though it just got a complete directory listing which doesn't list those files. An optimization the Apple iDisk service makes, is that it sometimes synthesizes those info files ahead of time. It then lists those synthesized files in the PROPFIND response together with the propery, which is the contents of the ".\_file" (if it would be present) in base64. It appears to only do this when the appledoubleinfo data is completely basic and is 82 bytes of size. This prevents the webdav clients from launching an additional PROPFIND request for every file prefixed with ".\_". Note that you cannot add an propery to a PROPSTAT element of a "file" itself, that's ignored, alas. macOS only accepts it on ".\_" files. There is not much information about this, but an Apple engineer mentioned it in https://lists.apple.com/archives/filesystem-dev/2009/Feb/msg00013.html There is a default "empty"-like response for a file that I found at https://github.com/DanRohde/webdavcgi/blob/master/lib/perl/WebDAV/Properties.pm So, what we _could_ do (but don't, yet) to optimize the macOS webdav client, when we reply to PROPFIND: - for each file that does NOT have a ".\_file" present - we synthesize a virtual response - for a virtual file with name ".\_file - with size: 82 bytes - that contains: AAUWBwACAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAJgAAACwAAAAJAAAAMgAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== The contents of this base64 string are explained at https://github.com/DanRohde/webdavcgi/blob/master/lib/perl/WebDAV/Properties.pm ... and they are: ``` appledoubleheader: Magic(4) Version(4) Filler(16) EntryCout(2) EntryDescriptor(id:4(2:resource fork),offset:4,length:4) EntryDescriptor(id:9 finder)... Finder Info(16+16) namespace: http://www.apple.com/webdav\_fs/props/ content: MIME::Base64(pack('H\*', '00051607'. '00020000' . ( '00' x 16 ) . '0002'. '00000002'. '00000026' . '0000002C'.'00000009'. '00000032' . '00000020' . ('00' x 32) )) ``` dav-server-0.11.0/doc/Apache-PUT-with-Content-Range.md000064400000000000000000000057071046102023000203230ustar 00000000000000# HTTP PUT-with-Content-Range support. The [mod_dav](https://httpd.apache.org/docs/2.4/mod/mod_dav.html) module of the [Apache web server](https://httpd.apache.org/) was one of the first implementations of [Webdav](https://tools.ietf.org/html/rfc4918). Ever since the first released version, it has had support for partial uploads using the Content-Range header with PUT requests. ## A sample request ```text PUT /file.txt Content-Length: 4 Content-Range: bytes 3-6/* ABCD ``` This request updates 'file.txt', specifically the bytes 3-6 (inclusive) to `ABCD`. There is no explicit support for appending to a file, that is simply done by writing just past the end of a file. For example, if a file has size 1000, and you want to append 4 bytes: ```text PUT /file.txt Content-Length: 4 Content-Range: bytes 1000-1003/* 1234 ``` ## Apache `mod_dav` behaviour: - The `Content-Range` header is required, and the syntax is `bytes START-END/LENGTH`. - END must be bigger than or equal to START. - LENGTH is parsed by Apache mod_dav, and it must either be a valid number or a `*` (star), but mod_dav otherwise ignores it. Since it is not clearly defined what LENGTH should be, always use `*`. - Neither the start, nor the end-byte have to be within the file's current size. - If the start-byte is beyond the file's current length, the space in between will be filled with NULL bytes (`0x00`). ## Notes - `bytes`, _not_ `bytes=`. - The `Content-Length` header is not required by the original Apache mod_dav implementation. The body must either have a valid Content-Length, or it must use the `Chunked` transfer encoding. It is *strongly encouraged* though to include Content-Length, so that it can be validated against the range before accepting the PUT request. - If the `Content-Length` header is present, its value must be equal to `END - START + 1`. ## Status codes ### The following status codes are used: Status code | Reason ----------- | ------ 200 or 204 | When the operation was successful 400 | Invalid `Content-Range` header 416 | If there was something wrong with the bytes, such as a `Content-Length` not matching with what was sent as the start and end bytes, or an end byte being lower than the start byte. 501 | Content-Range header present, but not supported. ## RECKOGNIZING PUT-with-Content-Range support (client). There is no official way to know if PUT-with-content-range is supported by a webserver. For a client it's probably best to do an OPTIONS request, and then check two things: - the `Server` header must contain the word `Apache` - the `DAV` header must contain ``. In that case, your are sure to talk to an Apache webserver with mod_dav enabled. ## IMPLEMENTING PUT-with-Content-Range support (server). Don't. Implement [sabredav-partialupdate](SABREDAV-partialupdate.md). ## MORE INFORMATION. https://blog.sphere.chronosempire.org.uk/2012/11/21/webdav-and-the-http-patch-nightmare dav-server-0.11.0/doc/SABREDAV-partialupdate.md000064400000000000000000000073041046102023000171000ustar 00000000000000# HTTP PATCH support This is a markdown translation of the document at [http://sabre.io/dav/http-patch/](http://sabre.io/dav/http-patch/) [© 2018 fruux GmbH](https://fruux.com/) The `Sabre\\DAV\\PartialUpdate\\Plugin` from the Sabre DAV library provides support for the HTTP PATCH method [RFC5789](http://tools.ietf.org/html/rfc5789). This allows you to update just a portion of a file, or append to a file. This document can be used as a spec for other implementors. There is some DAV-specific stuff in this document, but only in relation to the OPTIONS request. ## A sample request ``` PATCH /file.txt Content-Length: 4 Content-Type: application/x-sabredav-partialupdate X-Update-Range: bytes=3-6 ABCD ``` This request updates 'file.txt', specifically the bytes 3-6 (inclusive) to `ABCD`. If you just want to append to an existing file, use the following syntax: ``` PATCH /file.txt Content-Length: 4 Content-Type: application/x-sabredav-partialupdate X-Update-Range: append 1234 ``` The last request adds 4 bytes to the bottom of the file. ## The rules - The `Content-Length` header is required. - `X-Update-Range` is also required. - The `bytes` value is the exact same as the HTTP Range header. The two numbers are inclusive (so `3-6` means that bytes 3,4,5 and 6 will be updated). - Just like the HTTP Range header, the specified bytes is a 0-based index. - The `application/x-sabredav-partialupdate` must also be specified. - The end-byte is optional. - The start-byte cannot be omitted. - If the start byte is negative, it's calculated from the end of the file. So `-1` will update the last byte in the file. - Use `X-Update-Range: append` to add to the end of the file. - Neither the start, nor the end-byte have to be within the file's current size. - If the start-byte is beyond the file's current length, the space in between will be filled with NULL bytes (`0x00`). - The specification currently does not support multiple ranges. - If both start and end offsets are given, than both must be non-negative, and the end offset must be greater or equal to the start offset. ## More examples The following table illustrates most types of requests and what the end-result of them will be. It is assumed that the input file contains `1234567890`, and the request body always contains 4 dashes (`----`). X-Update-Range header | Result --------------------- | ------- `bytes=0-3` | `----567890` `bytes=1-4` | `1----67890` `bytes=0-` | `----567890` `bytes=-4` | `123456----` `bytes=-2` | `12345678----` `bytes=2-` | `12----7890` `bytes=12-` | `1234567890..----` `append` | `1234567890----` Please note that in the `bytes=12-` example, we used dots (`.`) to represent what are actually `NULL` bytes (so `0x00`). The null byte is not printable. ## Status codes ### The following status codes should be used: Status code | Reason ----------- | ------ 200 or 204 | When the operation was successful 400 | Invalid `X-Update-Range` header 411 | `Content-Length` header was not provided 415 | Unrecognized content-type, should be `application/x-sabredav-partialupdate` 416 | If there was something wrong with the bytes, such as a `Content-Length` not matching with what was sent as the start and end bytes, or an end byte being lower than the start byte. ## OPTIONS If you want to be compliant with SabreDAV's implementation of PATCH, you must also return 'sabredav-partialupdate' in the 'DAV:' header: ``` HTTP/1.1 204 No Content DAV: 1, 2, 3, sabredav-partialupdate, extended-mkcol ``` This is only required if you are adding this feature to a DAV server. For non-webdav implementations such as REST services this is optional. dav-server-0.11.0/examples/actix.rs000064400000000000000000000015651046102023000152640ustar 00000000000000use std::io; use actix_web::{App, HttpServer, web}; use dav_server::actix::*; use dav_server::{DavHandler, fakels::FakeLs, localfs::LocalFs}; pub async fn dav_handler(req: DavRequest, davhandler: web::Data) -> DavResponse { davhandler.handle(req.request).await.into() } #[actix_web::main] async fn main() -> io::Result<()> { env_logger::init(); let addr = "127.0.0.1:4918"; let dir = "/tmp"; let dav_server = DavHandler::builder() .filesystem(LocalFs::new(dir, false, false, false)) .locksystem(FakeLs::new()) .build_handler(); println!("actix-web example: listening on {} serving {}", addr, dir); HttpServer::new(move || { App::new() .app_data(web::Data::new(dav_server.clone())) .service(web::resource("/{tail:.*}").to(dav_handler)) }) .bind(addr)? .run() .await } dav-server-0.11.0/examples/auth.rs000064400000000000000000000113751046102023000151150ustar 00000000000000use std::{convert::Infallible, fmt::Display, net::SocketAddr, path::Path}; use futures_util::{StreamExt, stream}; use http::{Request, Response, StatusCode}; use hyper::{body::Incoming, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; use tokio::{net::TcpListener, task::spawn}; use dav_server::{ DavHandler, body::Body, davpath::DavPath, fakels::FakeLs, fs::{ DavDirEntry, DavFile, DavMetaData, FsFuture, FsResult, FsStream, GuardedFileSystem, OpenOptions, ReadDirMeta, }, localfs::LocalFs, }; /// The server example demonstrates a limited scope policy for access to the file system. /// Depending on the filter specified by the user in the request, one will receive only files or directories. /// For example, try this URLs: /// - dav://dirs:-@127.0.0.1:4918 — responds only with directories. /// - dav://files:-@127.0.0.1:4918 — responds only with files. #[tokio::main] async fn main() { env_logger::init(); let dir = "/tmp"; let addr: SocketAddr = ([127, 0, 0, 1], 4918).into(); let fs = FilteredFs::new(dir); let dav_server = DavHandler::builder() .filesystem(Box::new(fs) as _) .locksystem(FakeLs::new()) .build_handler(); let listener = TcpListener::bind(addr).await.unwrap(); println!("Listening {addr}"); loop { let (stream, _client_addr) = listener.accept().await.unwrap(); let dav_server = dav_server.clone(); let io = TokioIo::new(stream); spawn(async move { let service = service_fn(move |request| handle(request, dav_server.clone())); if let Err(err) = http1::Builder::new().serve_connection(io, service).await { eprintln!("Failed serving: {err:?}"); } }); } } async fn handle( request: Request, handler: DavHandler, ) -> Result, Infallible> { /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate static AUTH_CHALLENGE: &str = "Basic realm=\"Specify the directory entries' filter \ as a username: `dirs`, `files` or `all`; password — any string\""; let filter = match Filter::from_request(&request) { Ok(f) => f, Err(err) => { let response = Response::builder() .status(StatusCode::UNAUTHORIZED) .header("WWW-Authenticate", AUTH_CHALLENGE) .body(err.to_string().into()) .expect("Auth error response must be built fine"); return Ok(response); } }; Ok(handler .handle_guarded(request, "/principals/users/www_user".to_string(), filter) .await) } #[derive(Clone)] struct FilteredFs { inner: Box, } impl FilteredFs { fn new(dir: impl AsRef) -> Self { Self { inner: LocalFs::new(dir, false, false, false), } } } impl GuardedFileSystem for FilteredFs { fn open<'a>( &'a self, path: &'a DavPath, options: OpenOptions, _credentials: &'a Filter, ) -> FsFuture<'a, Box> { self.inner.open(path, options, &()) } fn read_dir<'a>( &'a self, path: &'a DavPath, meta: ReadDirMeta, filter: &'a Filter, ) -> FsFuture<'a, FsStream>> { Box::pin(async move { let mut stream = self.inner.read_dir(path, meta, &()).await?; let mut entries = Vec::default(); while let Some(entry) = stream.next().await { let entry = entry?; if filter.matches(entry.as_ref()).await? { entries.push(Ok(entry)); } } Ok(Box::pin(stream::iter(entries)) as _) }) } fn metadata<'a>( &'a self, path: &'a DavPath, _credentials: &'a Filter, ) -> FsFuture<'a, Box> { self.inner.metadata(path, &()) } } #[derive(Clone)] enum Filter { All, Files, Dirs, } impl Filter { fn from_request(request: &Request) -> Result> { use headers::{Authorization, HeaderMapExt, authorization::Basic}; let auth = request .headers() .typed_get::>() .ok_or(Box::new("please auth") as _)?; match auth.username() { "all" => Ok(Filter::All), "files" => Ok(Filter::Files), "dirs" => Ok(Filter::Dirs), _ => Err(Box::new("unexpected filter value") as _), } } async fn matches(&self, entry: &dyn DavDirEntry) -> FsResult { if let Filter::All = self { return Ok(true); } Ok(entry.is_dir().await? == matches!(self, Filter::Dirs)) } } dav-server-0.11.0/examples/axum.rs000064400000000000000000000023561046102023000151250ustar 00000000000000use axum::{Extension, Router, extract::Request, response::IntoResponse, routing::any}; use dav_server::{DavHandler, fakels::FakeLs, localfs::LocalFs}; use tokio::net::TcpListener; fn main() { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(async_main()); } async fn async_main() { if std::env::var("RUST_LOG").is_err() { unsafe { std::env::set_var("RUST_LOG", "debug"); } } env_logger::init(); let ip = "127.0.0.1"; let port = 4918; let addr = format!("{ip}:{port}"); let listener = TcpListener::bind(&addr).await.unwrap(); let dav = DavHandler::builder() .filesystem(LocalFs::new("/tmp", false, false, false)) .locksystem(FakeLs::new()) .strip_prefix("/dav") .build_handler(); let router = Router::new() .route("/dav", any(handle_dav)) .route("/dav/", any(handle_dav)) .route("/dav/{*path}", any(handle_dav)) .layer(Extension(dav)); log::info!("serve at http://{addr}"); axum::serve(listener, router).await.unwrap(); } async fn handle_dav(Extension(dav): Extension, req: Request) -> impl IntoResponse { dav.handle(req).await } dav-server-0.11.0/examples/caldav.rs000064400000000000000000000110151046102023000153750ustar 00000000000000//! CalDAV server example //! //! This example demonstrates how to set up a CalDAV server using the dav-server library. //! CalDAV is an extension of WebDAV for calendar data management. //! //! Usage: //! cargo run --example caldav --features caldav //! //! The server will be available at http://localhost:8080 //! You can connect to it using CalDAV clients like Thunderbird, Apple Calendar, etc. use axum::{ Extension, Router, body::Body, extract::Request, http::{HeaderValue, StatusCode}, middleware::{self, Next}, response::IntoResponse, routing::any, }; use chrono::Datelike; use dav_server::{DavHandler, caldav::DEFAULT_CALDAV_DIRECTORY, fakels::FakeLs, localfs}; use http_body_util::BodyExt; use std::sync::Arc; use tokio::net::TcpListener; #[tokio::main] async fn main() { env_logger::init(); let addr = "127.0.0.1:8080"; let dav_server = DavHandler::builder() .filesystem(localfs::LocalFs::new("/tmp", true, false, false)) .locksystem(FakeLs::new()) .autoindex(true) .build_handler(); let router = Router::new() .route("/.well-known/caldav", any(handle_caldav_redirect)) .route("/", any(handle_caldav)) .route("/{*path}", any(handle_caldav)) .layer(Extension(Arc::new(dav_server))) .layer(middleware::from_fn(log_request_middleware)); let listener = TcpListener::bind(&addr).await.unwrap(); println!("CalDAV server listening on http://{}", addr); println!( "Calendar collections can be accessed at http://{}{}", addr, DEFAULT_CALDAV_DIRECTORY ); println!(); println!( "NOTE: This example stores data in a temporary directory (/tmp). Data may be lost when the server stops or when temporary files are cleaned." ); println!(); println!("To create a calendar collection, use:"); println!( " curl -i -X MKCALENDAR http://{}{}/my-calendar/", addr, DEFAULT_CALDAV_DIRECTORY ); println!(); println!("To add a calendar event, use:"); println!( " curl -i -X PUT http://{}{}/my-calendar/event1.ics \\", addr, DEFAULT_CALDAV_DIRECTORY ); println!(" -H 'Content-Type: text/calendar' \\"); println!(" --data-binary @event.ics"); println!(); println!("Example event.ics content:"); println!("BEGIN:VCALENDAR"); println!("VERSION:2.0"); println!("PRODID:-//Example Corp//CalDAV Client//EN"); println!("BEGIN:VEVENT"); println!("UID:12345@example.com"); let next_year = chrono::Local::now().year_ce().1 + 1; println!("DTSTART:{}0101T120000Z", next_year); println!("DTEND:{}0101T130000Z", next_year); println!("SUMMARY:New Year Meeting"); println!("DESCRIPTION:Planning meeting for the new year"); println!("END:VEVENT"); println!("END:VCALENDAR"); axum::serve(listener, router).await.unwrap(); } async fn handle_caldav_redirect() -> ( StatusCode, [(axum::http::header::HeaderName, HeaderValue); 1], ) { ( StatusCode::MOVED_PERMANENTLY, [( axum::http::header::LOCATION, HeaderValue::from_static(DEFAULT_CALDAV_DIRECTORY), )], ) } async fn handle_caldav( Extension(dav): Extension>, req: Request, ) -> impl IntoResponse { dav.handle(req).await } async fn log_request_middleware(request: Request, next: Next) -> impl IntoResponse { // Print request line and headers println!("\n========== CLIENT REQUEST =========="); println!("{} {}", request.method(), request.uri(),); println!("--- Headers ---"); for (name, value) in request.headers() { println!("{}: {}", name, value.to_str().unwrap_or("")); } // Read and print body let (parts, body) = request.into_parts(); let collected = body.collect().await.unwrap_or_default(); let body_bytes = collected.to_bytes(); if !body_bytes.is_empty() { println!("--- Body ---"); if let Ok(body_str) = std::str::from_utf8(&body_bytes) { println!("{}", body_str); } else { println!("", body_bytes.len()); } } println!("====================================\n"); // Reconstruct request with body let request = axum::http::Request::from_parts(parts, Body::from(body_bytes)); next.run(request).await } #[cfg(not(feature = "caldav"))] fn main() { eprintln!("This example requires the 'caldav' feature to be enabled."); eprintln!("Run with: cargo run --example caldav --features caldav"); std::process::exit(1); } dav-server-0.11.0/examples/carddav.rs000064400000000000000000000110061046102023000155470ustar 00000000000000//! CardDAV server example //! //! This example demonstrates how to set up a CardDAV server using the dav-server library. //! CardDAV is an extension of WebDAV for contact/address book data management. //! //! Usage: //! cargo run --example carddav --features carddav //! //! The server will be available at http://localhost:8080 //! You can connect to it using CardDAV clients like Thunderbird, Apple Contacts, etc. use axum::{ Extension, Router, body::Body, extract::Request, http::{HeaderValue, StatusCode}, middleware::{self, Next}, response::IntoResponse, routing::any, }; use dav_server::{DavHandler, carddav::DEFAULT_CARDDAV_DIRECTORY, fakels::FakeLs, localfs}; use http_body_util::BodyExt; use std::sync::Arc; use tokio::net::TcpListener; #[tokio::main] async fn main() { env_logger::init(); let addr = "127.0.0.1:8080"; let dav_server = DavHandler::builder() .filesystem(localfs::LocalFs::new("/tmp", true, false, false)) .locksystem(FakeLs::new()) .autoindex(true) // For a real world application you would have your own GuardedFilesystem // and use server.handle_guarded(req, format!("/principals/users/{user_name}"), credentials) .principal("/addressbooks") .build_handler(); let router = Router::new() .route("/.well-known/carddav", any(handle_carddav_redirect)) .route("/", any(handle_carddav)) .route("/{*path}", any(handle_carddav)) .layer(Extension(Arc::new(dav_server))) .layer(middleware::from_fn(log_request_middleware)); let listener = TcpListener::bind(&addr).await.unwrap(); println!("CardDAV server listening on http://{}", addr); println!( "Address book collections can be accessed at http://{}{}", addr, DEFAULT_CARDDAV_DIRECTORY ); println!(); println!( "NOTE: This example stores data in a temporary directory (/tmp). Data may be lost when the server stops or when temporary files are cleaned." ); println!(); println!("To create an address book collection, use:"); println!( " curl -i -X MKADDRESSBOOK http://{}{}/my-contacts/", addr, DEFAULT_CARDDAV_DIRECTORY ); println!(); println!("To add a contact, use:"); println!( " curl -i -X PUT http://{}{}/my-contacts/contact1.vcf \\", addr, DEFAULT_CARDDAV_DIRECTORY ); println!(" -H 'Content-Type: text/vcard' \\"); println!(" --data-binary @contact.vcf"); println!(); println!("Example contact.vcf content:"); println!("BEGIN:VCARD"); println!("VERSION:3.0"); println!("UID:12345@example.com"); println!("FN:John Doe"); println!("N:Doe;John;;;"); println!("EMAIL:john.doe@example.com"); println!("TEL:+1-555-123-4567"); println!("END:VCARD"); axum::serve(listener, router).await.unwrap(); } async fn handle_carddav_redirect() -> ( StatusCode, [(axum::http::header::HeaderName, HeaderValue); 1], ) { ( StatusCode::MOVED_PERMANENTLY, [( axum::http::header::LOCATION, HeaderValue::from_static(DEFAULT_CARDDAV_DIRECTORY), )], ) } async fn handle_carddav( Extension(dav): Extension>, req: Request, ) -> impl IntoResponse { dav.handle(req).await } async fn log_request_middleware(request: Request, next: Next) -> impl IntoResponse { // Print request line and headers println!("\n========== CLIENT REQUEST =========="); println!("{} {}", request.method(), request.uri(),); println!("--- Headers ---"); for (name, value) in request.headers() { println!("{}: {}", name, value.to_str().unwrap_or("")); } // Read and print body let (parts, body) = request.into_parts(); let collected = body.collect().await.unwrap_or_default(); let body_bytes = collected.to_bytes(); if !body_bytes.is_empty() { println!("--- Body ---"); if let Ok(body_str) = std::str::from_utf8(&body_bytes) { println!("{}", body_str); } else { println!("", body_bytes.len()); } } println!("====================================\n"); // Reconstruct request with body let request = axum::http::Request::from_parts(parts, Body::from(body_bytes)); next.run(request).await } #[cfg(not(feature = "carddav"))] fn main() { eprintln!("This example requires the 'carddav' feature to be enabled."); eprintln!("Run with: cargo run --example carddav --features carddav"); std::process::exit(1); } dav-server-0.11.0/examples/hyper.rs000064400000000000000000000033611046102023000152770ustar 00000000000000use std::{convert::Infallible, net::SocketAddr}; use hyper::{server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; use dav_server::{DavHandler, fakels::FakeLs, localfs::LocalFs}; #[tokio::main] async fn main() { env_logger::init(); let dir = "/tmp"; let addr: SocketAddr = ([127, 0, 0, 1], 4918).into(); let dav_server = DavHandler::builder() .filesystem(LocalFs::new(dir, false, false, false)) .locksystem(FakeLs::new()) .build_handler(); let listener = TcpListener::bind(addr).await.unwrap(); println!("Listening {addr}"); // We start a loop to continuously accept incoming connections loop { let (stream, _) = listener.accept().await.unwrap(); let dav_server = dav_server.clone(); // Use an adapter to access something implementing `tokio::io` traits as if they implement // `hyper::rt` IO traits. let io = TokioIo::new(stream); // Spawn a tokio task to serve multiple connections concurrently tokio::task::spawn(async move { // Finally, we bind the incoming connection to our `hello` service if let Err(err) = http1::Builder::new() // `service_fn` converts our function in a `Service` .serve_connection( io, service_fn({ move |req| { let dav_server = dav_server.clone(); async move { Ok::<_, Infallible>(dav_server.handle(req).await) } } }), ) .await { eprintln!("Failed serving: {err:?}"); } }); } } dav-server-0.11.0/examples/sample-litmus-server.rs000064400000000000000000000110071046102023000202440ustar 00000000000000// // Sample application. // // Listens on localhost:4918, plain http, no ssl. // Connect to http://localhost:4918/ // use std::{convert::Infallible, error::Error, net::SocketAddr}; use clap::Parser; use headers::{Authorization, HeaderMapExt, authorization::Basic}; use hyper::{server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; use dav_server::{DavConfig, DavHandler, body::Body, fakels, localfs, memfs, memls}; #[derive(Clone)] struct Server { dh: DavHandler, auth: bool, } impl Server { pub fn new(directory: String, memls: bool, fakels: bool, auth: bool) -> Self { let mut config = DavHandler::builder(); if !directory.is_empty() { config = config.filesystem(localfs::LocalFs::new(directory, true, true, true)); } else { config = config.filesystem(memfs::MemFs::new()); }; if fakels { config = config.locksystem(fakels::FakeLs::new()); } if memls { config = config.locksystem(memls::MemLs::new()); } Server { dh: config.build_handler(), auth, } } async fn handle( &self, req: hyper::Request, ) -> Result, Infallible> { let user = if self.auth { // we want the client to authenticate. match req.headers().typed_get::>() { Some(Authorization(basic)) => Some(basic.username().to_string()), None => { // return a 401 reply. let response = hyper::Response::builder() .status(401) .header("WWW-Authenticate", "Basic realm=\"foo\"") .body(Body::from("please auth".to_string())) .unwrap(); return Ok(response); } } } else { None }; if let Some(user) = user { let config = DavConfig::new().principal(user); Ok(self.dh.handle_with(config, req).await) } else { Ok(self.dh.handle(req).await) } } } #[derive(Debug, clap::Parser)] #[command(about, version)] struct Cli { /// port to listen on #[arg(short = 'p', long, default_value = "4918")] port: u16, /// local directory to serve #[arg(short = 'd', long)] dir: Option, /// serve from ephemeral memory filesystem #[arg(short = 'm', long)] memfs: bool, /// use ephemeral memory locksystem #[arg(short = 'l', long)] memls: bool, /// use fake memory locksystem #[arg(short = 'f', long)] fakels: bool, /// require basic authentication #[arg(short = 'a', long)] auth: bool, } #[tokio::main] async fn main() -> Result<(), Box> { env_logger::init(); let args = Cli::parse(); let (dir, name) = match args.dir.as_ref() { Some(dir) => (dir.as_str(), dir.as_str()), None => ("", "memory filesystem"), }; let auth = args.auth; let memls = args.memfs || args.memls; let fakels = args.fakels; let dav_server = Server::new(dir.to_string(), memls, fakels, auth); let port = args.port; let addr: SocketAddr = ([0, 0, 0, 0], port).into(); let listener = TcpListener::bind(addr).await?; println!("Serving {} on {}", name, port); // We start a loop to continuously accept incoming connections loop { let (stream, _) = listener.accept().await?; let dav_server = dav_server.clone(); // Use an adapter to access something implementing `tokio::io` traits as if they implement // `hyper::rt` IO traits. let io = TokioIo::new(stream); // Spawn a tokio task to serve multiple connections concurrently tokio::task::spawn(async move { // Finally, we bind the incoming connection to our `hello` service if let Err(err) = http1::Builder::new() // `service_fn` converts our function in a `Service` .serve_connection( io, service_fn({ move |req| { let dav_server = dav_server.clone(); async move { dav_server.clone().handle(req).await } } }), ) .await { eprintln!("Error serving connection: {:?}", err); } }); } } dav-server-0.11.0/examples/warp.rs000064400000000000000000000005431046102023000151200ustar 00000000000000use dav_server::warp::dav_dir; use std::net::SocketAddr; #[tokio::main] async fn main() { env_logger::init(); let dir = "/tmp"; let addr: SocketAddr = ([127, 0, 0, 1], 4918).into(); println!("warp example: listening on {:?} serving {}", addr, dir); let warpdav = dav_dir(dir, true, true); warp::serve(warpdav).run(addr).await; } dav-server-0.11.0/src/actix.rs000064400000000000000000000116501046102023000142310ustar 00000000000000//! Adapters to use the standard `http` types with Actix. //! //! Using the adapters in this crate, it's easy to build a webdav //! handler for actix: //! //! ```no_run //! use dav_server::{DavHandler, actix::DavRequest, actix::DavResponse}; //! use actix_web::web; //! //! pub async fn dav_handler(req: DavRequest, davhandler: web::Data) -> DavResponse { //! davhandler.handle(req.request).await.into() //! } //! ``` //! use std::{ convert::TryInto, future, io, pin::Pin, task::{Context, Poll}, }; use actix_web::body::BoxBody; use actix_web::error::PayloadError; use actix_web::{Error, FromRequest, HttpRequest, HttpResponse, dev}; use bytes::Bytes; use futures_util::Stream; use pin_project_lite::pin_project; /// http::Request compatibility. /// /// Wraps `http::Request` and implements `actix_web::FromRequest`. pub struct DavRequest { pub request: http::Request, prefix: Option, } impl DavRequest { /// Returns the request path minus the tail. pub fn prefix(&self) -> Option<&str> { self.prefix.as_deref() } } impl FromRequest for DavRequest { type Error = Error; type Future = future::Ready>; fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { let mut builder = http::Request::builder() .method(req.method().as_ref()) .uri(req.uri().to_string()) .version(from_actix_http_version(req.version())); for (name, value) in req.headers().iter() { builder = builder.header(name.as_str(), value.as_ref()); } let path = req.path(); let tail = req.match_info().unprocessed(); let prefix = match &path[..path.len() - tail.len()] { "" | "/" => None, x => Some(x.to_string()), }; let body = DavBody { body: payload.take(), }; let stdreq = DavRequest { request: builder.body(body).unwrap(), prefix, }; future::ready(Ok(stdreq)) } } pin_project! { /// Body type for `DavRequest`. /// /// It wraps actix's `PayLoad` and implements `http_body::Body`. pub struct DavBody { #[pin] body: dev::Payload, } } impl http_body::Body for DavBody { type Data = Bytes; type Error = io::Error; fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { self.project() .body .poll_next(cx) .map_ok(http_body::Frame::data) .map_err(|err| match err { PayloadError::Incomplete(Some(err)) => err, PayloadError::Incomplete(None) => io::ErrorKind::BrokenPipe.into(), PayloadError::Io(err) => err, err => io::Error::other(format!("{err:?}")), }) } } /// `http::Response` compatibility. /// /// Wraps `http::Response` and implements actix_web::Responder. pub struct DavResponse(pub http::Response); impl From> for DavResponse { fn from(resp: http::Response) -> DavResponse { DavResponse(resp) } } impl actix_web::Responder for DavResponse { type Body = BoxBody; fn respond_to(self, _req: &HttpRequest) -> HttpResponse { use crate::body::{Body, BodyType}; let (parts, body) = self.0.into_parts(); let mut builder = HttpResponse::build(parts.status.as_u16().try_into().unwrap()); for (name, value) in parts.headers.into_iter() { builder.append_header((name.unwrap().as_str(), value.as_ref())); } // I noticed that actix-web returns an empty chunked body // (\r\n0\r\n\r\n) and _no_ Transfer-Encoding header on // a 204 statuscode. It's probably because of // builder.streaming(). So only use builder.streaming() // on actual streaming replies. match body.inner { BodyType::Bytes(None) => builder.body(""), BodyType::Bytes(Some(b)) => builder.body(b), BodyType::Empty => builder.body(""), b @ BodyType::AsyncStream(..) => builder.streaming(Body { inner: b }), } } } /// Converts HTTP version from `actix_web` version of `http` crate while `actix_web` remains on old version. /// https://github.com/actix/actix-web/issues/3384 fn from_actix_http_version(v: actix_web::http::Version) -> http::Version { match v { actix_web::http::Version::HTTP_3 => http::Version::HTTP_3, actix_web::http::Version::HTTP_2 => http::Version::HTTP_2, actix_web::http::Version::HTTP_11 => http::Version::HTTP_11, actix_web::http::Version::HTTP_10 => http::Version::HTTP_10, actix_web::http::Version::HTTP_09 => http::Version::HTTP_09, v => unreachable!("unexpected HTTP version {:?}", v), } } dav-server-0.11.0/src/async_stream.rs000064400000000000000000000120551046102023000156110ustar 00000000000000//! Use an [async block][async] to produce items for a stream. //! //! Example: //! //! ```rust ignore //! use futures_util::StreamExt; //! use futures_executor::block_on; //! # use dav_server::async_stream; //! use async_stream::AsyncStream; //! //! let mut strm = AsyncStream::::new(|mut tx| async move { //! for i in 0u8..10 { //! tx.send(i).await; //! } //! Ok(()) //! }); //! //! let fut = async { //! let mut count = 0; //! while let Some(item) = strm.next().await { //! println!("{:?}", item); //! count += 1; //! } //! assert!(count == 10); //! }; //! block_on(fut); //! //! ``` //! //! The stream will produce a `Result` where the `Item` //! is an item sent with [tx.send(item)][send]. Any errors returned by //! the async closure will be returned as an error value on //! the stream. //! //! On success the async closure should return `Ok(())`. //! //! [async]: https://rust-lang.github.io/async-book/getting_started/async_await_primer.html //! [send]: async_stream/struct.Sender.html#method.send //! use std::cell::Cell; use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; use std::rc::Rc; use std::task::{Context, Poll}; use futures_util::Stream; /// Future returned by the Sender.send() method. /// /// Completes when the item is sent. #[must_use] pub struct SenderFuture { is_ready: bool, } impl SenderFuture { fn new() -> SenderFuture { SenderFuture { is_ready: false } } } impl Future for SenderFuture { type Output = (); fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { if self.is_ready { Poll::Ready(()) } else { self.is_ready = true; Poll::Pending } } } // Only internally used by one AsyncStream and never shared // in any other way, so we don't have to use Arc>. /// Type of the sender passed as first argument into the async closure. pub struct Sender(Rc>>, PhantomData); unsafe impl Sync for Sender {} unsafe impl Send for Sender {} impl Sender { fn new(item_opt: Option) -> Sender { Sender(Rc::new(Cell::new(item_opt)), PhantomData::) } // note that this is NOT impl Clone for Sender, it's private. fn clone(&self) -> Sender { Sender(self.0.clone(), PhantomData::) } /// Send one item to the stream. pub fn send(&mut self, item: T) -> SenderFuture where T: Into, { self.0.set(Some(item.into())); SenderFuture::new() } } /// An abstraction around a future, where the /// future can internally loop and yield items. /// /// AsyncStream::new() takes a [Future][Future] ([async closure][async], usually) /// and AsyncStream then implements a [futures 0.3 Stream][Stream]. /// /// [async]: https://rust-lang.github.io/async-book/getting_started/async_await_primer.html /// [Future]: https://doc.rust-lang.org/std/future/trait.Future.html /// [Stream]: https://docs.rs/futures/0.3/futures/stream/trait.Stream.html #[must_use] pub struct AsyncStream { item: Sender, #[allow(clippy::type_complexity)] fut: Option> + 'static + Send>>>, } impl AsyncStream { /// Create a new stream from a closure returning a Future 0.3, /// or an "async closure" (which is the same). /// /// The closure is passed one argument, the sender, which has a /// method "send" that can be called to send a item to the stream. pub fn new(f: F) -> Self where F: FnOnce(Sender) -> R, R: Future> + Send + 'static, Item: 'static, { let sender = Sender::new(None); AsyncStream:: { item: sender.clone(), fut: Some(Box::pin(f(sender))), } } } /// Stream implementation for Futures 0.3. impl Stream for AsyncStream { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>> { let pollres = { let fut = self.fut.as_mut().unwrap(); fut.as_mut().poll(cx) }; match pollres { // If the future returned Poll::Ready, that signals the end of the stream. Poll::Ready(Ok(_)) => Poll::Ready(None), Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))), Poll::Pending => { // Pending means that some sub-future returned pending. That sub-future // _might_ have been the SenderFuture returned by Sender.send, so // check if there is an item available in self.item. let mut item = self.item.0.replace(None); if item.is_none() { Poll::Pending } else { Poll::Ready(Some(Ok(item.take().unwrap()))) } } } } } dav-server-0.11.0/src/body.rs000064400000000000000000000060361046102023000140600ustar 00000000000000//! Definitions for the Request and Response bodies. use std::error::Error as StdError; use std::io; use std::pin::Pin; use std::task::{Context, Poll}; use bytes::{Buf, Bytes}; use futures_util::stream::Stream; use http_body::Body as HttpBody; use pin_project_lite::pin_project; use crate::async_stream::AsyncStream; /// Body is returned by the webdav handler, and implements both `Stream` /// and `http_body::Body`. pub struct Body { pub inner: BodyType, } pub enum BodyType { Bytes(Option), AsyncStream(AsyncStream), Empty, } impl Body { /// Return an empty body. pub fn empty() -> Body { Body { inner: BodyType::Empty, } } } impl Stream for Body { type Item = io::Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { match self.inner { BodyType::Bytes(ref mut strm) => Poll::Ready(strm.take().map(Ok)), BodyType::AsyncStream(ref mut strm) => { let strm = Pin::new(strm); strm.poll_next(cx) } BodyType::Empty => Poll::Ready(None), } } } impl HttpBody for Body { type Data = Bytes; type Error = io::Error; fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { self.poll_next(cx).map_ok(http_body::Frame::data) } } impl From for Body { fn from(t: String) -> Body { Body { inner: BodyType::Bytes(Some(Bytes::from(t))), } } } impl From<&str> for Body { fn from(t: &str) -> Body { Body { inner: BodyType::Bytes(Some(Bytes::from(t.to_string()))), } } } impl From for Body { fn from(t: Bytes) -> Body { Body { inner: BodyType::Bytes(Some(t)), } } } impl From> for Body { fn from(s: AsyncStream) -> Body { Body { inner: BodyType::AsyncStream(s), } } } pin_project! { // // A struct that contains a Stream, and implements http_body::Body. // pub(crate) struct StreamBody { #[pin] body: B, } } impl HttpBody for StreamBody where ReqData: Buf + Send, ReqError: StdError + Send + Sync + 'static, ReqBody: Stream>, { type Data = ReqData; type Error = ReqError; fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { let this = self.project(); this.body.poll_next(cx).map_ok(http_body::Frame::data) } } impl StreamBody where ReqData: Buf + Send, ReqError: StdError + Send + Sync + 'static, ReqBody: Stream>, { pub fn new(body: ReqBody) -> StreamBody { StreamBody { body } } } dav-server-0.11.0/src/caldav.rs000064400000000000000000000222771046102023000143620ustar 00000000000000//! CalDAV (Calendaring Extensions to WebDAV) support //! //! This module provides CalDAV functionality on top of the base WebDAV implementation. //! CalDAV is defined in RFC 4791 and provides standardized access to calendar data //! using the iCalendar format. #[cfg(feature = "caldav")] use icalendar::Calendar; use xmltree::Element; use crate::davpath::DavPath; // Re-export shared filter types pub use crate::dav_filters::{ParameterFilter, TextMatch}; // CalDAV XML namespaces pub const NS_CALDAV_URI: &str = "urn:ietf:params:xml:ns:caldav"; pub const NS_CALENDARSERVER_URI: &str = "http://calendarserver.org/ns/"; // CalDAV property names pub const CALDAV_PROPERTIES: &[&str] = &[ "C:calendar-description", "C:calendar-timezone", "C:supported-calendar-component-set", "C:supported-calendar-data", "C:max-resource-size", "C:min-date-time", "C:max-date-time", "C:max-instances", "C:max-attendees-per-instance", "C:calendar-home-set", "C:calendar-user-address-set", "C:schedule-inbox-URL", "C:schedule-outbox-URL", ]; /// The default caldav directory, which is being used for the preprovided filesystems. Path is without trailing slash pub const DEFAULT_CALDAV_NAME: &str = "calendars"; pub const DEFAULT_CALDAV_DIRECTORY: &str = "/calendars"; pub const DEFAULT_CALDAV_DIRECTORY_ENDSLASH: &str = "/calendars/"; /// CalDAV resource types #[derive(Debug, Clone, PartialEq)] pub enum CalDavResourceType { Calendar, ScheduleInbox, ScheduleOutbox, CalendarObject, Regular, } /// CalDAV component types supported in a calendar collection #[derive(Debug, Clone, PartialEq)] pub enum CalendarComponentType { VEvent, VTodo, VJournal, VFreeBusy, VTimezone, VAlarm, } impl CalendarComponentType { pub fn as_str(&self) -> &'static str { match self { CalendarComponentType::VEvent => "VEVENT", CalendarComponentType::VTodo => "VTODO", CalendarComponentType::VJournal => "VJOURNAL", CalendarComponentType::VFreeBusy => "VFREEBUSY", CalendarComponentType::VTimezone => "VTIMEZONE", CalendarComponentType::VAlarm => "VALARM", } } } /// CalDAV calendar collection properties #[derive(Debug, Clone)] pub struct CalendarProperties { pub description: Option, pub timezone: Option, pub supported_components: Vec, pub max_resource_size: Option, pub color: Option, pub display_name: Option, } impl Default for CalendarProperties { fn default() -> Self { Self { description: None, timezone: None, supported_components: vec![ CalendarComponentType::VEvent, CalendarComponentType::VTodo, CalendarComponentType::VJournal, CalendarComponentType::VFreeBusy, ], max_resource_size: Some(1024 * 1024), // 1MB default color: None, display_name: None, } } } /// Calendar query filters for REPORT requests #[derive(Debug, Clone)] pub struct CalendarQuery { pub comp_filter: Option, pub time_range: Option, pub properties: Vec, } #[derive(Debug, Clone)] pub struct ComponentFilter { pub name: String, pub is_not_defined: bool, pub time_range: Option, pub prop_filters: Vec, pub comp_filters: Vec, } /// CalDAV property filter with time-range support /// /// Note: CalDAV property filters include time-range which is not present /// in the shared ParameterFilter. CardDAV has a similar struct without time_range. #[derive(Debug, Clone)] pub struct PropertyFilter { pub name: String, pub is_not_defined: bool, pub text_match: Option, pub time_range: Option, pub param_filters: Vec, } #[derive(Debug, Clone)] pub struct TimeRange { /// ISO 8601 format pub start: Option, /// ISO 8601 format pub end: Option, } /// CalDAV REPORT request types #[derive(Debug, Clone)] pub enum CalDavReportType { CalendarQuery(CalendarQuery), CalendarMultiget { hrefs: Vec }, FreeBusyQuery { time_range: TimeRange }, } /// Helper functions for CalDAV XML generation pub fn create_supported_calendar_component_set(components: &[CalendarComponentType]) -> Element { let mut elem = Element::new("C:supported-calendar-component-set"); elem.namespace = Some(NS_CALDAV_URI.to_string()); for comp in components { let mut comp_elem = Element::new("C:comp"); comp_elem.namespace = Some(NS_CALDAV_URI.to_string()); comp_elem .attributes .insert("name".to_string(), comp.as_str().to_string()); elem.children.push(xmltree::XMLNode::Element(comp_elem)); } elem } pub fn create_supported_calendar_data() -> Element { let mut elem = Element::new("C:supported-calendar-data"); elem.namespace = Some(NS_CALDAV_URI.to_string()); let mut calendar_data = Element::new("C:calendar-data"); calendar_data.namespace = Some(NS_CALDAV_URI.to_string()); calendar_data .attributes .insert("content-type".to_string(), "text/calendar".to_string()); calendar_data .attributes .insert("version".to_string(), "2.0".to_string()); elem.children.push(xmltree::XMLNode::Element(calendar_data)); elem } pub fn create_calendar_home_set(prefix: &str, path: &str) -> Element { let mut elem = Element::new("C:calendar-home-set"); elem.namespace = Some(NS_CALDAV_URI.to_string()); let mut href = Element::new("D:href"); href.namespace = Some("DAV:".to_string()); href.children .push(xmltree::XMLNode::Text(format!("{prefix}{path}"))); elem.children.push(xmltree::XMLNode::Element(href)); elem } /// Check if a path is within the default CalDAV directory. Expects path without prefix. pub(crate) fn is_path_in_caldav_directory(dav_path: &DavPath) -> bool { let path_string = dav_path.to_string(); path_string.len() > DEFAULT_CALDAV_DIRECTORY_ENDSLASH.len() && path_string.starts_with(DEFAULT_CALDAV_DIRECTORY_ENDSLASH) } /// Check if a resource is a calendar collection based on resource type pub fn is_calendar_collection(resource_type: &[Element]) -> bool { resource_type .iter() .any(|elem| elem.name == "calendar" && elem.namespace.as_deref() == Some(NS_CALDAV_URI)) } /// Check if content appears to be iCalendar data pub fn is_calendar_data(content: &[u8]) -> bool { if !content.starts_with(b"BEGIN:VCALENDAR") { return false; } let trimmed = content.trim_ascii_end(); trimmed.ends_with(b"END:VCALENDAR") } /// Validate iCalendar data using the icalendar crate /// /// This function validates that the content is a well-formed iCalendar object. /// Use this function in your application layer to validate calendar data /// before or after writing to the filesystem. /// /// # Example /// /// ```ignore /// use dav_server::caldav::validate_calendar_data; /// /// let ical = "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\n..."; /// match validate_calendar_data(ical) { /// Ok(_) => println!("Valid iCalendar"), /// Err(e) => println!("Invalid iCalendar: {}", e), /// } /// ``` #[cfg(feature = "caldav")] pub fn validate_calendar_data(content: &str) -> Result { content .parse::() .map_err(|e| format!("Invalid iCalendar data: {}", e)) } /// Extract the UID from calendar data /// /// Handles both standard `UID:value` and properties with parameters. pub fn extract_calendar_uid(content: &str) -> Option { for line in content.lines() { let line = line.trim(); // Handle simple UID:VALUE if let Some(value) = line.strip_prefix("UID:") { return Some(value.to_string()); } // Handle UID with parameters: UID;PARAMS:VALUE if let Some(rest) = line.strip_prefix("UID;") && let Some(colon_pos) = rest.find(':') { return Some(rest[colon_pos + 1..].to_string()); } } None } /// Generate a simple calendar collection resource type XML pub fn calendar_resource_type() -> Vec { let mut collection = Element::new("D:collection"); collection.namespace = Some("DAV:".to_string()); let mut calendar = Element::new("C:calendar"); calendar.namespace = Some(NS_CALDAV_URI.to_string()); vec![collection, calendar] } /// Generate schedule inbox resource type XML pub fn schedule_inbox_resource_type() -> Vec { let mut collection = Element::new("D:collection"); collection.namespace = Some("DAV:".to_string()); let mut schedule_inbox = Element::new("C:schedule-inbox"); schedule_inbox.namespace = Some(NS_CALDAV_URI.to_string()); vec![collection, schedule_inbox] } /// Generate schedule outbox resource type XML pub fn schedule_outbox_resource_type() -> Vec { let mut collection = Element::new("D:collection"); collection.namespace = Some("DAV:".to_string()); let mut schedule_outbox = Element::new("C:schedule-outbox"); schedule_outbox.namespace = Some(NS_CALDAV_URI.to_string()); vec![collection, schedule_outbox] } dav-server-0.11.0/src/carddav.rs000064400000000000000000000175141046102023000145320ustar 00000000000000//! CardDAV (vCard Extensions to WebDAV) support //! //! This module provides CardDAV functionality on top of the base WebDAV implementation. //! CardDAV is defined in RFC 6352 and provides standardized access to address book data //! using the vCard format. #[cfg(feature = "carddav")] use calcard::vcard::VCard; use xmltree::Element; use crate::davpath::DavPath; // Re-export shared filter types pub use crate::dav_filters::{ParameterFilter, TextMatch}; // CardDAV XML namespaces pub const NS_CARDDAV_URI: &str = "urn:ietf:params:xml:ns:carddav"; /// The default carddav directory, which is being used for the preprovided filesystems. Path is without trailing slash pub const DEFAULT_CARDDAV_NAME: &str = "addressbooks"; pub const DEFAULT_CARDDAV_DIRECTORY: &str = "/addressbooks"; pub const DEFAULT_CARDDAV_DIRECTORY_ENDSLASH: &str = "/addressbooks/"; /// Default maximum resource size for address book entries (1MB) pub const DEFAULT_MAX_RESOURCE_SIZE: u64 = 1024 * 1024; /// CardDAV address book collection properties #[derive(Debug, Clone)] pub struct AddressBookProperties { pub description: Option, pub max_resource_size: Option, pub display_name: Option, } impl Default for AddressBookProperties { fn default() -> Self { Self { description: None, max_resource_size: Some(DEFAULT_MAX_RESOURCE_SIZE), display_name: None, } } } /// Address book query filters for REPORT requests #[derive(Debug, Clone)] pub struct AddressBookQuery { pub prop_filter: Option, pub properties: Vec, pub limit: Option, } /// CardDAV property filter /// /// Note: CardDAV property filters are similar to CalDAV but without time_range. /// Both use the shared TextMatch and ParameterFilter types. #[derive(Debug, Clone)] pub struct PropertyFilter { pub name: String, pub is_not_defined: bool, pub text_match: Option, pub param_filters: Vec, } /// CardDAV REPORT request types #[derive(Debug, Clone)] pub enum CardDavReportType { AddressBookQuery(AddressBookQuery), AddressBookMultiget { hrefs: Vec }, } /// Helper functions for CardDAV XML generation pub fn create_supported_address_data() -> Element { let mut elem = Element::new("CARD:supported-address-data"); elem.namespace = Some(NS_CARDDAV_URI.to_string()); let mut address_data = Element::new("CARD:address-data-type"); address_data.namespace = Some(NS_CARDDAV_URI.to_string()); address_data .attributes .insert("content-type".to_string(), "text/vcard".to_string()); address_data .attributes .insert("version".to_string(), "3.0".to_string()); elem.children.push(xmltree::XMLNode::Element(address_data)); // Also support vCard 4.0 let mut address_data_v4 = Element::new("CARD:address-data-type"); address_data_v4.namespace = Some(NS_CARDDAV_URI.to_string()); address_data_v4 .attributes .insert("content-type".to_string(), "text/vcard".to_string()); address_data_v4 .attributes .insert("version".to_string(), "4.0".to_string()); elem.children .push(xmltree::XMLNode::Element(address_data_v4)); elem } pub fn create_addressbook_home_set(prefix: &str, path: &str) -> Element { log::debug!("create_addressbook_home_set prefix: {:#?}", prefix); log::debug!("create_addressbook_home_set path: {:#?}", path); let mut elem = Element::new("CARD:addressbook-home-set"); elem.namespace = Some(NS_CARDDAV_URI.to_string()); let mut href = Element::new("D:href"); href.namespace = Some("DAV:".to_string()); href.children .push(xmltree::XMLNode::Text(format!("{prefix}{path}"))); elem.children.push(xmltree::XMLNode::Element(href)); elem } /// Check if a path is within the default CardDAV directory. Expects path without prefix. pub(crate) fn is_path_in_carddav_directory(dav_path: &DavPath) -> bool { let path_string = dav_path.to_string(); path_string.len() > DEFAULT_CARDDAV_DIRECTORY_ENDSLASH.len() && path_string.starts_with(DEFAULT_CARDDAV_DIRECTORY_ENDSLASH) } /// Check if content appears to be vCard data pub fn is_vcard_data(content: &[u8]) -> bool { if !content.starts_with(b"BEGIN:VCARD") { return false; } let trimmed = content.trim_ascii_end(); trimmed.ends_with(b"END:VCARD") } /// Validate vCard data using the calcard crate /// /// This function validates that the content is a well-formed vCard. /// Use this function in your application layer to validate vCard data /// before or after writing to the filesystem. /// /// # Example /// /// ```ignore /// use dav_server::carddav::validate_vcard_data; /// /// let vcard = "BEGIN:VCARD\nVERSION:3.0\nFN:Test\nEND:VCARD"; /// match validate_vcard_data(vcard) { /// Ok(_) => println!("Valid vCard"), /// Err(e) => println!("Invalid vCard: {}", e), /// } /// ``` #[cfg(feature = "carddav")] pub fn validate_vcard_data(content: &str) -> Result { VCard::parse(content).map_err(|e| format!("Invalid vCard data: {:?}", e)) } /// Validate vCard data and check for required properties /// /// This is a stricter validation that ensures the vCard has required properties /// like VERSION and FN (formatted name) as required by RFC 6350. /// /// Returns an error message describing what's missing or invalid. #[cfg(feature = "carddav")] pub fn validate_vcard_strict(content: &str) -> Result<(), String> { // First, try to parse the vCard let vcard = validate_vcard_data(content)?; // Check for VERSION property if vcard.version().is_none() { return Err("Missing required VERSION property".to_string()); } // FN is required in vCard 3.0 and 4.0 // Check if FN exists in the parsed vCard or via string extraction if extract_vcard_fn(content).is_none() { return Err("Missing required FN (formatted name) property".to_string()); } Ok(()) } /// Extract the UID from vCard data /// /// Handles both standard `UID:value` and grouped properties like `item1.UID:value`. /// Also handles properties with parameters like `UID;VALUE=TEXT:value`. pub fn extract_vcard_uid(content: &str) -> Option { for line in content.lines() { let line = line.trim(); if let Some(uid) = extract_vcard_property_value(line, "UID") { return Some(uid); } } None } /// Extract the FN (formatted name) from vCard data /// /// Handles both standard `FN:value` and grouped properties like `item1.FN:value`. /// Also handles properties with parameters like `FN;CHARSET=UTF-8:value`. pub fn extract_vcard_fn(content: &str) -> Option { for line in content.lines() { let line = line.trim(); if let Some(fn_value) = extract_vcard_property_value(line, "FN") { return Some(fn_value); } } None } /// Helper function to extract a vCard property value, handling groups and parameters /// /// Supports formats: /// - `PROPERTY:value` /// - `group.PROPERTY:value` /// - `PROPERTY;param=val:value` /// - `group.PROPERTY;param=val:value` fn extract_vcard_property_value(line: &str, property_name: &str) -> Option { // Find the colon that separates the property name from the value let colon_pos = line.find(':')?; let property_part = &line[..colon_pos]; let value = &line[colon_pos + 1..]; // Check if property part matches (with optional group prefix and parameters) // The property name is before any semicolon (which starts parameters) let name_part = property_part.split(';').next()?; // Check for group prefix (e.g., "item1.UID" -> "UID") let actual_name = if let Some(dot_pos) = name_part.find('.') { &name_part[dot_pos + 1..] } else { name_part }; if actual_name.eq_ignore_ascii_case(property_name) { Some(value.to_string()) } else { None } } dav-server-0.11.0/src/conditional.rs000064400000000000000000000204401046102023000154210ustar 00000000000000use std::time::{Duration, SystemTime, UNIX_EPOCH}; use headers::HeaderMapExt; use http::{Method, StatusCode}; use crate::davheaders::{self, ETag}; use crate::davpath::DavPath; use crate::fs::{DavMetaData, GuardedFileSystem}; use crate::ls::DavLockSystem; type Request = http::Request<()>; // SystemTime has nanosecond precision. Round it down to the // nearest second, because an HttpDate has second precision. fn round_time(tm: impl Into) -> SystemTime { let tm = tm.into(); match tm.duration_since(UNIX_EPOCH) { Ok(d) => UNIX_EPOCH + Duration::from_secs(d.as_secs()), Err(_) => tm, } } pub(crate) fn ifrange_match( hdr: &davheaders::IfRange, tag: Option<&davheaders::ETag>, date: Option, ) -> bool { match *hdr { davheaders::IfRange::Date(ref d) => match date { Some(date) => round_time(date) == round_time(*d), None => false, }, davheaders::IfRange::ETag(ref t) => match tag { Some(tag) => t == tag, None => false, }, } } pub(crate) fn etaglist_match( tags: &davheaders::ETagList, exists: bool, tag: Option<&davheaders::ETag>, ) -> bool { match *tags { davheaders::ETagList::Star => exists, davheaders::ETagList::Tags(ref t) => match tag { Some(tag) => t.iter().any(|x| x == tag), None => false, }, } } // Handle the if-headers: RFC 7232, HTTP/1.1 Conditional Requests. pub(crate) fn http_if_match(req: &Request, meta: Option<&dyn DavMetaData>) -> Option { let file_modified = meta.and_then(|m| m.modified().ok()); if let Some(r) = req.headers().typed_get::() { let etag = meta.and_then(ETag::from_meta); if !etaglist_match(&r.0, meta.is_some(), etag.as_ref()) { trace!("precondition fail: If-Match {r:?}"); return Some(StatusCode::PRECONDITION_FAILED); } } else if let Some(r) = req.headers().typed_get::() { match file_modified { None => return Some(StatusCode::PRECONDITION_FAILED), Some(file_modified) => { if round_time(file_modified) > round_time(r) { trace!("precondition fail: If-Unmodified-Since {r:?}"); return Some(StatusCode::PRECONDITION_FAILED); } } } } if let Some(r) = req.headers().typed_get::() { let etag = meta.and_then(ETag::from_meta); if etaglist_match(&r.0, meta.is_some(), etag.as_ref()) { trace!("precondition fail: If-None-Match {r:?}"); if req.method() == Method::GET || req.method() == Method::HEAD { return Some(StatusCode::NOT_MODIFIED); } else { return Some(StatusCode::PRECONDITION_FAILED); } } } else if let Some(r) = req.headers().typed_get::() && (req.method() == Method::GET || req.method() == Method::HEAD) && let Some(file_modified) = file_modified && round_time(file_modified) <= round_time(r) { trace!("not-modified If-Modified-Since {r:?}"); return Some(StatusCode::NOT_MODIFIED); } None } // handle the If header: RFC4918, 10.4. If Header // // returns true if the header was not present, or if any of the iflists // evaluated to true. Also returns a Vec of StateTokens that we encountered. // // caller should set the http status to 412 PreconditionFailed if // the return value from this function is false. // pub(crate) async fn dav_if_match<'a, C>( req: &'a Request, fs: &'a (dyn GuardedFileSystem + 'static), ls: &'a Option>, path: &'a DavPath, credentials: &C, ) -> (bool, Vec) where C: Clone + Send + Sync + 'static, { let mut tokens: Vec = Vec::new(); let mut any_list_ok = false; let r = match req.headers().typed_get::() { Some(r) => r, None => return (true, tokens), }; for iflist in r.0.iter() { // save and return all statetokens that we encountered. let toks = iflist.conditions.iter().filter_map(|c| match c.item { davheaders::IfItem::StateToken(ref t) => Some(t.to_owned()), _ => None, }); tokens.extend(toks); // skip over if a previous list already evaluated to true. if any_list_ok { continue; } // find the resource that this list is about. let mut pa: Option = None; let (p, valid) = match iflist.resource_tag { Some(ref url) => { match DavPath::from_str_and_prefix(url.path(), path.prefix()) { Ok(p) => { // anchor davpath in pa. let p: &DavPath = pa.get_or_insert(p); (p, true) } Err(_) => (path, false), } } None => (path, true), }; // now process the conditions. they must all be true. let mut list_ok = false; for cond in iflist.conditions.iter() { let cond_ok = match cond.item { davheaders::IfItem::StateToken(ref s) => { // tokens in DAV: namespace always evaluate to false (10.4.8) if !valid || s.starts_with("DAV:") { false } else { match *ls { Some(ref ls) => { let tokens: Vec = vec![s.to_owned()]; ls.check(p, None, true, false, &tokens).await.is_ok() } None => false, } } } davheaders::IfItem::ETag(ref tag) => { if !valid { // invalid location, so always false. false } else { match fs.metadata(p, credentials).await { Ok(meta) => { // exists and may have metadata .. if let Some(mtag) = ETag::from_meta(meta.as_ref()) { tag == &mtag } else { false } } Err(_) => { // metadata error, fail. false } } } } }; if cond_ok == cond.not { list_ok = false; break; } list_ok = true; } if list_ok { any_list_ok = true; } } if !any_list_ok { trace!("precondition fail: If {:?}", r.0); } (any_list_ok, tokens) } // Handle both the HTTP conditional If: headers, and the webdav If: header. pub(crate) async fn if_match<'a, C>( req: &'a Request, meta: Option<&'a (dyn DavMetaData + 'static)>, fs: &'a (dyn GuardedFileSystem + 'static), ls: &'a Option>, path: &'a DavPath, credentials: &C, ) -> Option where C: Clone + Send + Sync + 'static, { match dav_if_match(req, fs, ls, path, credentials).await { (true, _) => {} (false, _) => return Some(StatusCode::PRECONDITION_FAILED), } http_if_match(req, meta) } // Like if_match, but also returns all "associated state-tokens" pub(crate) async fn if_match_get_tokens<'a, C>( req: &'a Request, meta: Option<&'a (dyn DavMetaData + 'static)>, fs: &'a (dyn GuardedFileSystem + 'static), ls: &'a Option>, path: &'a DavPath, credentials: &C, ) -> Result, StatusCode> where C: Clone + Send + Sync + 'static, { if let Some(code) = http_if_match(req, meta) { return Err(code); } match dav_if_match(req, fs, ls, path, credentials).await { (true, v) => Ok(v), (false, _) => Err(StatusCode::PRECONDITION_FAILED), } } dav-server-0.11.0/src/dav_filters.rs000064400000000000000000000027631046102023000154300ustar 00000000000000//! Common filter types shared between CalDAV and CardDAV //! //! This module contains filter structures used by both CalDAV (RFC 4791) //! and CardDAV (RFC 6352) REPORT requests. /// Text matching filter for property values /// /// Used in both CalDAV and CardDAV for matching text content in properties. #[derive(Debug, Clone, Default)] pub struct TextMatch { /// The text to match against pub text: String, /// Collation to use for comparison (e.g., "i;ascii-casemap") pub collation: Option, /// If true, the match condition is negated pub negate_condition: bool, /// Match type for CardDAV: "equals", "contains", "starts-with", "ends-with" /// CalDAV uses "contains" by default if not specified pub match_type: Option, } /// Parameter filter for matching property parameters /// /// Used in both CalDAV and CardDAV for filtering based on property parameters /// (e.g., TYPE=HOME on a TEL property). #[derive(Debug, Clone)] pub struct ParameterFilter { /// Name of the parameter to filter on (e.g., "TYPE") pub name: String, /// If true, the parameter must NOT be defined pub is_not_defined: bool, /// Text match filter for the parameter value pub text_match: Option, } impl ParameterFilter { /// Create a new parameter filter with the given name pub fn new(name: impl Into) -> Self { Self { name: name.into(), is_not_defined: false, text_match: None, } } } dav-server-0.11.0/src/davhandler.rs000064400000000000000000000460111046102023000152300ustar 00000000000000// // This module contains the main entry point of the library, // DavHandler. // use std::error::Error as StdError; use std::io; use std::pin::pin; use std::sync::Arc; use bytes::{self, buf::Buf}; use derive_where::derive_where; use futures_util::stream::Stream; use headers::HeaderMapExt; use http::{Request, Response, StatusCode}; use http_body::Body as HttpBody; use http_body_util::BodyExt; use crate::body::{Body, StreamBody}; use crate::davheaders; use crate::davpath::DavPath; use crate::util::{DavMethod, DavMethodSet, dav_method}; use crate::DavResult; use crate::errors::DavError; use crate::fs::*; use crate::ls::*; use crate::voidfs::{VoidFs, is_voidfs}; /// WebDAV request handler. /// /// The [`new`](Self::new) and [`builder`](Self::builder) methods are used to instantiate a handler. /// /// The [`handle`](Self::handle) and [`handle_with`](Self::handle_with) methods do the actual work. /// /// Type parameter `C` represents credentials for file systems with access control. #[derive(Clone)] #[derive_where(Default)] pub struct DavHandler { pub(crate) config: Arc>, } /// Configuration of the handler. #[derive(Clone)] #[derive_where(Default)] pub struct DavConfig { // Prefix to be stripped off when handling request. pub(crate) prefix: Option, // Filesystem backend. pub(crate) fs: Option>>, // Locksystem backend. pub(crate) ls: Option>, // Set of allowed methods (None means "all methods") pub(crate) allow: Option, // Principal is webdav speak for "user", used to give locks an owner (if a locksystem is // active). pub(crate) principal: Option, // Hide symbolic links? `None` maps to `true`. pub(crate) hide_symlinks: Option, // Does GET on a directory return indexes. pub(crate) autoindex: Option, // index.html pub(crate) indexfile: Option, // read buffer size in bytes pub(crate) read_buf_size: Option, // Does GET on a file return 302 redirect. pub(crate) redirect: Option, } impl DavConfig { /// Create a new configuration builder. pub fn new() -> Self { Self::default() } /// Use the configuration that was built to generate a [`DavHandler`]. pub fn build_handler(self) -> DavHandler { DavHandler { config: Arc::new(self), } } /// Prefix to be stripped off before translating the rest of /// the request path to a filesystem path. pub fn strip_prefix(self, prefix: impl Into) -> Self { let mut this = self; this.prefix = Some(prefix.into()); this } /// Set the filesystem to use. pub fn filesystem(self, fs: Box>) -> Self { let mut this = self; this.fs = Some(fs); this } /// Set the locksystem to use. pub fn locksystem(self, ls: Box) -> Self { let mut this = self; this.ls = Some(ls); this } /// Which methods to allow (default is all methods). pub fn methods(self, allow: DavMethodSet) -> Self { let mut this = self; this.allow = Some(allow); this } /// Set the name of the "webdav principal". This will be the owner of any created locks. pub fn principal(self, principal: impl Into) -> Self { let mut this = self; this.principal = Some(principal.into()); this } /// Hide symbolic links (default is true) pub fn hide_symlinks(self, hide: bool) -> Self { let mut this = self; this.hide_symlinks = Some(hide); this } /// Does a GET on a directory produce a directory index. pub fn autoindex(self, autoindex: bool) -> Self { let mut this = self; this.autoindex = Some(autoindex); this } /// Indexfile to show (index.html, usually). pub fn indexfile(self, indexfile: impl Into) -> Self { let mut this = self; this.indexfile = Some(indexfile.into()); this } /// Read buffer size in bytes pub fn read_buf_size(self, size: usize) -> Self { let mut this = self; this.read_buf_size = Some(size); this } pub fn redirect(self, redirect: bool) -> Self { let mut this = self; this.redirect = Some(redirect); this } fn merge(&self, new: Self) -> Self { Self { prefix: new.prefix.or_else(|| self.prefix.clone()), fs: new.fs.or_else(|| self.fs.clone()), ls: new.ls.or_else(|| self.ls.clone()), allow: new.allow.or(self.allow), principal: new.principal.or_else(|| self.principal.clone()), hide_symlinks: new.hide_symlinks.or(self.hide_symlinks), autoindex: new.autoindex.or(self.autoindex), indexfile: new.indexfile.or_else(|| self.indexfile.clone()), read_buf_size: new.read_buf_size.or(self.read_buf_size), redirect: new.redirect.or(self.redirect), } } } // The actual inner struct. // // At the start of the request, DavConfig is used to generate // a DavInner struct. DavInner::handle then handles the request. pub(crate) struct DavInner { pub prefix: String, pub fs: Box>, pub ls: Option>, pub allow: Option, pub principal: Option, pub hide_symlinks: Option, pub autoindex: Option, pub indexfile: Option, pub read_buf_size: Option, pub redirect: Option, pub credentials: C, } impl DavHandler { /// Create a new `DavHandler`. /// /// This returns a DavHandler with an empty configuration. That's only /// useful if you use the `handle_with` method instead of `handle`. /// Normally you should create a new `DavHandler` using `DavHandler::build` /// and configure at least the filesystem, and probably the strip_prefix. pub fn new() -> Self { Self { config: Default::default(), } } /// Return a configuration builder. pub fn builder() -> DavConfig { DavConfig::new() } /// Process a WebDAV request to a file system with access control. pub async fn handle_guarded( &self, req: Request, principal: String, credentials: C, ) -> Response where ReqData: Buf + Send + 'static, ReqError: StdError + Send + Sync + 'static, ReqBody: HttpBody, { let mut inner = DavInner::new(self.config.as_ref().clone(), credentials); inner.principal = Some(principal); inner.handle(req).await } } impl DavHandler { /// Process a WebDAV request to a file system without access control. pub async fn handle(&self, req: Request) -> Response where ReqData: Buf + Send + 'static, ReqError: StdError + Send + Sync + 'static, ReqBody: HttpBody, { let inner = DavInner::new(self.config.as_ref().clone(), ()); inner.handle(req).await } /// Handle a webdav request, overriding parts of the config. /// /// For example, the `principal` can be set for this request. /// /// Or, the default config has no locksystem, and you pass in /// a fake locksystem (`FakeLs`) because this is a request from a /// windows or macos client that needs to see locking support. pub async fn handle_with( &self, config: DavConfig, req: Request, ) -> Response where ReqData: Buf + Send + 'static, ReqError: StdError + Send + Sync + 'static, ReqBody: HttpBody, { let inner = DavInner::new(self.config.merge(config), ()); inner.handle(req).await } /// Handles a request with a `Stream` body instead of a `HttpBody`. /// Used with webserver frameworks that have not /// opted to use the `http_body` crate just yet. #[doc(hidden)] pub async fn handle_stream( &self, req: Request, ) -> Response where ReqData: Buf + Send + 'static, ReqError: StdError + Send + Sync + 'static, ReqBody: Stream>, { let req = { let (parts, body) = req.into_parts(); Request::from_parts(parts, StreamBody::new(body)) }; let inner = DavInner::new(self.config.as_ref().clone(), ()); inner.handle(req).await } /// Handles a request with a `Stream` body instead of a `HttpBody`. #[doc(hidden)] pub async fn handle_stream_with( &self, config: DavConfig, req: Request, ) -> Response where ReqData: Buf + Send + 'static, ReqError: StdError + Send + Sync + 'static, ReqBody: Stream>, { let req = { let (parts, body) = req.into_parts(); Request::from_parts(parts, StreamBody::new(body)) }; let inner = DavInner::new(self.config.merge(config), ()); inner.handle(req).await } } impl DavInner where C: Clone + Send + Sync + 'static, { pub fn new(cfg: DavConfig, credentials: C) -> Self { let DavConfig { prefix, fs, ls, allow, principal, hide_symlinks, autoindex, indexfile, read_buf_size, redirect, } = cfg; Self { prefix: prefix.unwrap_or_default(), fs: fs.unwrap_or_else(|| VoidFs::::new()), ls, allow, principal, hide_symlinks, autoindex, indexfile, read_buf_size, redirect, credentials, } } // helper. pub(crate) async fn has_parent<'a>(&'a self, path: &'a DavPath) -> bool { let p = path.parent(); self.fs .metadata(&p, &self.credentials) .await .map(|m| m.is_dir()) .unwrap_or(false) } // helper. pub(crate) fn path(&self, req: &Request<()>) -> DavPath { // This never fails (has been checked before) DavPath::from_uri_and_prefix(req.uri(), &self.prefix).unwrap() } // See if this is a directory and if so, if we have // to fixup the path by adding a slash at the end. pub(crate) fn fixpath( &self, res: &mut Response, path: &mut DavPath, meta: Box, ) -> Box { if meta.is_dir() && !path.is_collection() { path.add_slash(); let newloc = path.with_prefix().as_url_string(); res.headers_mut() .typed_insert(davheaders::ContentLocation(newloc)); } meta } // drain request body and return length. pub(crate) async fn read_request( &self, body: ReqBody, max_size: usize, ) -> DavResult> where ReqBody: HttpBody, ReqData: Buf + Send + 'static, ReqError: StdError + Send + Sync + 'static, { let mut data = Vec::new(); let mut body = pin!(body); while let Some(res) = body.frame().await { let mut data_frame = res.map_err(|_| { DavError::IoError(io::Error::new( io::ErrorKind::UnexpectedEof, "UnexpectedEof", )) })?; let Some(buf) = data_frame.data_mut() else { continue; }; while buf.has_remaining() { if data.len() + buf.remaining() > max_size { return Err(StatusCode::PAYLOAD_TOO_LARGE.into()); } let b = buf.chunk(); let l = b.len(); data.extend_from_slice(b); buf.advance(l); } } Ok(data) } // internal dispatcher. async fn handle(self, req: Request) -> Response where ReqBody: HttpBody, ReqData: Buf + Send + 'static, ReqError: StdError + Send + Sync + 'static, { let is_ms = req .headers() .get("user-agent") .and_then(|s| s.to_str().ok()) .map(|s| s.contains("Microsoft")) .unwrap_or(false); // Turn any DavError results into a HTTP error response. match self.handle2(req).await { Ok(resp) => { debug!("== END REQUEST result OK"); resp } Err(err) => { debug!("== END REQUEST result {err:?}"); let mut resp = Response::builder(); if is_ms && err.statuscode() == StatusCode::NOT_FOUND { // This is an attempt to convince Windows to not // cache a 404 NOT_FOUND for 30-60 seconds. // // That is a problem since windows caches the NOT_FOUND in a // case-insensitive way. So if "www" does not exist, but "WWW" does, // and you do a "dir www" and then a "dir WWW" the second one // will fail. // // Ofcourse the below is not sufficient. Fixes welcome. resp = resp .header("Cache-Control", "no-store, no-cache, must-revalidate") .header("Progma", "no-cache") .header("Expires", "0") .header("Vary", "*"); } resp = resp.header("Content-Length", "0").status(err.statuscode()); if err.must_close() { resp = resp.header("connection", "close"); } resp.body(Body::empty()).unwrap() } } } // internal dispatcher part 2. async fn handle2( mut self, req: Request, ) -> DavResult> where ReqBody: HttpBody, ReqData: Buf + Send + 'static, ReqError: StdError + Send + Sync + 'static, { let (req, body) = { let (parts, body) = req.into_parts(); (Request::from_parts(parts, ()), body) }; // debug when running the webdav litmus tests. if log_enabled!(log::Level::Debug) && let Some(t) = req.headers().typed_get::() { debug!("X-Litmus: {t:?}"); } // translate HTTP method to Webdav method. let method = match dav_method(req.method()) { Ok(m) => m, Err(e) => { debug!("refusing method {} request {}", req.method(), req.uri()); return Err(e); } }; // See if method makes sense if we don't have a filesystem. if is_voidfs::(&self.fs) { match method { DavMethod::Options => { if self .allow .as_ref() .map(|a| a.contains(DavMethod::Options)) .unwrap_or(true) { let mut a = DavMethodSet::none(); a.add(DavMethod::Options); self.allow = Some(a); } } _ => { debug!("no filesystem: method not allowed on request {}", req.uri()); return Err(DavError::StatusClose(StatusCode::METHOD_NOT_ALLOWED)); } } } // see if method is allowed. if let Some(ref a) = self.allow && !a.contains(method) { debug!( "method {} not allowed on request {}", req.method(), req.uri() ); return Err(DavError::StatusClose(StatusCode::METHOD_NOT_ALLOWED)); } // make sure the request path is valid. let path = DavPath::from_uri_and_prefix(req.uri(), &self.prefix)?; // PUT is the only handler that reads the body itself. All the // other handlers either expected no body, or a pre-read Vec. let (body_strm, body_data) = match method { DavMethod::Put | DavMethod::Patch => (Some(body), Vec::new()), _ => (None, self.read_request(body, 65536).await?), }; // Not all methods accept a body. match method { DavMethod::Put | DavMethod::Patch | DavMethod::PropFind | DavMethod::PropPatch | DavMethod::Lock | DavMethod::Report | DavMethod::MkCalendar | DavMethod::MkAddressbook => {} _ => { if !body_data.is_empty() { return Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into()); } } } debug!("== START REQUEST {method:?} {path}"); match method { DavMethod::Options => self.handle_options(&req).await, DavMethod::PropFind => self.handle_propfind(&req, &body_data).await, DavMethod::PropPatch => self.handle_proppatch(&req, &body_data).await, DavMethod::MkCol => self.handle_mkcol(&req).await, DavMethod::Delete => self.handle_delete(&req).await, DavMethod::Lock => self.handle_lock(&req, &body_data).await, DavMethod::Unlock => self.handle_unlock(&req).await, DavMethod::Head | DavMethod::Get => self.handle_get(&req).await, DavMethod::Copy | DavMethod::Move => self.handle_copymove(&req, method).await, DavMethod::Put | DavMethod::Patch => self.handle_put(&req, body_strm.unwrap()).await, #[cfg(any(feature = "caldav", feature = "carddav"))] DavMethod::Report => self.handle_report(&req, &body_data).await, #[cfg(feature = "caldav")] DavMethod::MkCalendar => self.handle_mkcalendar(&req, &body_data).await, #[cfg(feature = "carddav")] DavMethod::MkAddressbook => self.handle_mkaddressbook(&req, &body_data).await, #[cfg(not(any(feature = "caldav", feature = "carddav")))] DavMethod::Report => Err(DavError::StatusClose(StatusCode::NOT_IMPLEMENTED)), #[cfg(not(feature = "caldav"))] DavMethod::MkCalendar => Err(DavError::StatusClose(StatusCode::NOT_IMPLEMENTED)), #[cfg(not(feature = "carddav"))] DavMethod::MkAddressbook => Err(DavError::StatusClose(StatusCode::NOT_IMPLEMENTED)), } } } dav-server-0.11.0/src/davheaders.rs000064400000000000000000000553321046102023000152340ustar 00000000000000use std::convert::TryFrom; use std::fmt::Display; use std::str::FromStr; use headers::Header; use http::header::{HeaderName, HeaderValue}; use url::Url; use crate::fs::DavMetaData; pub static DEPTH: HeaderName = HeaderName::from_static("depth"); pub static TIMEOUT: HeaderName = HeaderName::from_static("timeout"); pub static OVERWRITE: HeaderName = HeaderName::from_static("overwrite"); pub static DESTINATION: HeaderName = HeaderName::from_static("destination"); pub static ETAG: HeaderName = HeaderName::from_static("etag"); pub static IF_RANGE: HeaderName = HeaderName::from_static("if-range"); pub static IF_MATCH: HeaderName = HeaderName::from_static("if-match"); pub static IF_NONE_MATCH: HeaderName = HeaderName::from_static("if-none-match"); pub static X_UPDATE_RANGE: HeaderName = HeaderName::from_static("x-update-range"); pub static IF: HeaderName = HeaderName::from_static("if"); pub static CONTENT_LANGUAGE: HeaderName = HeaderName::from_static("content-language"); // helper. fn one<'i, I>(values: &mut I) -> Result<&'i HeaderValue, headers::Error> where I: Iterator, { let v = values.next().ok_or_else(invalid)?; if values.next().is_some() { Err(invalid()) } else { Ok(v) } } // helper fn invalid() -> headers::Error { headers::Error::invalid() } // helper fn map_invalid(_e: impl std::error::Error) -> headers::Error { headers::Error::invalid() } macro_rules! header { ($tname:ident, $hname:ident, $sname:expr_2021) => { pub static $hname: HeaderName = HeaderName::from_static($sname); #[derive(Debug, Clone, PartialEq)] pub struct $tname(pub String); impl Header for $tname { fn name() -> &'static HeaderName { &$hname } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { one(values)? .to_str() .map(|x| $tname(x.to_owned())) .map_err(map_invalid) } fn encode(&self, values: &mut E) where E: Extend, { let value = HeaderValue::from_str(&self.0).unwrap(); values.extend(std::iter::once(value)) } } }; } header!(ContentType, CONTENT_TYPE, "content-type"); header!(ContentLocation, CONTENT_LOCATION, "content-location"); header!(LockToken, LOCK_TOKEN, "lock-token"); header!(XLitmus, X_LITMUS, "x-litmus"); /// - "Depth" header for PROPFIND requests. See the items for its response behaviour #[derive(Debug, Copy, Clone, PartialEq)] pub enum Depth { /// DEFAULT / no Depth header: no target resource, Depth 1 children Default, /// Depth 0: only target resource, no children Zero, /// Depth 1: target resource, Depth 1 children One, /// Infinite depth or `Depth` > 1 are not to be supported and return `NotImplemented` for performance reasons Infinity, } impl Header for Depth { fn name() -> &'static HeaderName { &DEPTH } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { let value = one(values)?; match value.as_bytes() { b"0" => Ok(Depth::Zero), b"1" => Ok(Depth::One), b"infinity" | b"Infinity" => Ok(Depth::Infinity), _ => Err(invalid()), } } fn encode(&self, values: &mut E) where E: Extend, { let value = match *self { Depth::Default => "", Depth::Zero => "0", Depth::One => "1", Depth::Infinity => "Infinity", }; values.extend(std::iter::once(HeaderValue::from_static(value))); } } /// Content-Language header. #[derive(Debug, Clone, PartialEq)] pub struct ContentLanguage(headers::Vary); impl ContentLanguage { #[allow(dead_code)] pub fn iter_langs(&self) -> impl Iterator { self.0.iter_strs() } } impl TryFrom<&str> for ContentLanguage { type Error = headers::Error; fn try_from(value: &str) -> Result { let value = HeaderValue::from_str(value).map_err(map_invalid)?; let mut values = std::iter::once(&value); ContentLanguage::decode(&mut values) } } impl Header for ContentLanguage { fn name() -> &'static HeaderName { &CONTENT_LANGUAGE } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { let h = headers::Vary::decode(values)?; for lang in h.iter_strs() { let lang = lang.as_bytes(); // **VERY** rudimentary check ... let ok = lang.len() == 2 || (lang.len() > 4 && lang[2] == b'-'); if !ok { return Err(invalid()); } } Ok(ContentLanguage(h)) } fn encode(&self, values: &mut E) where E: Extend, { self.0.encode(values) } } #[derive(Debug, Clone, PartialEq)] pub enum DavTimeout { Seconds(u32), Infinite, } #[derive(Debug, Clone)] pub struct Timeout(pub Vec); impl Header for Timeout { fn name() -> &'static HeaderName { &TIMEOUT } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { let value = one(values)?; let mut v = Vec::new(); let words = value.to_str().map_err(map_invalid)?.split(','); for word in words { let w = match word { "Infinite" => DavTimeout::Infinite, _ if word.starts_with("Second-") => { let num = &word[7..]; match num.parse::() { Err(_) => return Err(invalid()), Ok(n) => DavTimeout::Seconds(n), } } _ => return Err(invalid()), }; v.push(w); } Ok(Timeout(v)) } fn encode(&self, values: &mut E) where E: Extend, { let mut first = false; let mut value = String::new(); for s in &self.0 { if !first { value.push_str(", "); } first = false; match *s { DavTimeout::Seconds(n) => value.push_str(&format!("Second-{n}")), DavTimeout::Infinite => value.push_str("Infinite"), } } values.extend(std::iter::once(HeaderValue::from_str(&value).unwrap())); } } #[derive(Debug, Clone, PartialEq)] pub struct Destination(pub String); impl Header for Destination { fn name() -> &'static HeaderName { &DESTINATION } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { let s = one(values)?.to_str().map_err(map_invalid)?; if s.starts_with('/') { return Ok(Destination(s.to_string())); } if let Ok(url) = s.parse::() { return Ok(Destination(url.path().to_string())); } Err(invalid()) } fn encode(&self, values: &mut E) where E: Extend, { values.extend(std::iter::once(HeaderValue::from_str(&self.0).unwrap())); } } #[derive(Debug, Clone, PartialEq)] pub struct Overwrite(pub bool); impl Header for Overwrite { fn name() -> &'static HeaderName { &OVERWRITE } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { let line = one(values)?; match line.as_bytes() { b"F" => Ok(Overwrite(false)), b"T" => Ok(Overwrite(true)), _ => Err(invalid()), } } fn encode(&self, values: &mut E) where E: Extend, { let value = match self.0 { true => "T", false => "F", }; values.extend(std::iter::once(HeaderValue::from_static(value))); } } #[derive(Debug, Clone)] pub struct ETag { tag: String, weak: bool, } impl ETag { #[allow(dead_code)] pub fn new(weak: bool, t: impl Into) -> Result { let t = t.into(); if t.contains('\"') { Err(invalid()) } else { let w = if weak { "W/" } else { "" }; Ok(ETag { tag: format!("{w}\"{t}\""), weak, }) } } pub fn from_meta(meta: &dyn DavMetaData) -> Option { let tag = meta.etag()?; Some(ETag { tag: format!("\"{tag}\""), weak: false, }) } #[allow(dead_code)] pub fn is_weak(&self) -> bool { self.weak } } impl FromStr for ETag { type Err = headers::Error; fn from_str(t: &str) -> Result { let (weak, s) = if let Some(t) = t.strip_prefix("W/") { (true, t) } else { (false, t) }; if s.starts_with('\"') && s.ends_with('\"') && !s[1..s.len() - 1].contains('\"') { Ok(ETag { tag: t.to_owned(), weak, }) } else { Err(invalid()) } } } impl TryFrom<&HeaderValue> for ETag { type Error = headers::Error; fn try_from(value: &HeaderValue) -> Result { let s = value.to_str().map_err(map_invalid)?; ETag::from_str(s) } } impl Display for ETag { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "\"{}\"", self.tag) } } impl PartialEq for ETag { fn eq(&self, other: &Self) -> bool { !self.weak && !other.weak && self.tag == other.tag } } impl Header for ETag { fn name() -> &'static HeaderName { &ETAG } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { let value = one(values)?; ETag::try_from(value) } fn encode(&self, values: &mut E) where E: Extend, { values.extend(std::iter::once(HeaderValue::from_str(&self.tag).unwrap())); } } #[derive(Debug, Clone, PartialEq)] pub enum IfRange { ETag(ETag), Date(headers::Date), } impl Header for IfRange { fn name() -> &'static HeaderName { &IF_RANGE } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { let value = one(values)?; let mut iter = std::iter::once(value); if let Ok(tm) = headers::Date::decode(&mut iter) { return Ok(IfRange::Date(tm)); } let mut iter = std::iter::once(value); if let Ok(et) = ETag::decode(&mut iter) { return Ok(IfRange::ETag(et)); } Err(invalid()) } fn encode(&self, values: &mut E) where E: Extend, { match *self { IfRange::Date(ref d) => d.encode(values), IfRange::ETag(ref t) => t.encode(values), } } } #[derive(Debug, Clone, PartialEq)] pub enum ETagList { Tags(Vec), Star, } #[derive(Debug, Clone, PartialEq)] pub struct IfMatch(pub ETagList); #[derive(Debug, Clone, PartialEq)] pub struct IfNoneMatch(pub ETagList); // Decode a list of etags. This is not entirely correct, we should // actually use a real parser. E.g. we don't handle comma's in // etags correctly - but we never generated those anyway. fn decode_etaglist<'i, I>(values: &mut I) -> Result where I: Iterator, { let mut v = Vec::new(); let mut count = 0usize; for value in values { let s = value.to_str().map_err(map_invalid)?; if s.trim() == "*" { return Ok(ETagList::Star); } for t in s.split(',') { // Simply skip misformed etags, they will never match. if let Ok(t) = ETag::from_str(t.trim()) { v.push(t); } } count += 1; } if count != 0 { Ok(ETagList::Tags(v)) } else { Err(invalid()) } } fn encode_etaglist(m: &ETagList, values: &mut E) where E: Extend, { let value = match *m { ETagList::Star => "*".to_string(), ETagList::Tags(ref t) => t .iter() .map(|t| t.tag.as_str()) .collect::>() .join(", "), }; values.extend(std::iter::once(HeaderValue::from_str(&value).unwrap())); } impl Header for IfMatch { fn name() -> &'static HeaderName { &IF_MATCH } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { Ok(IfMatch(decode_etaglist(values)?)) } fn encode(&self, values: &mut E) where E: Extend, { encode_etaglist(&self.0, values) } } impl Header for IfNoneMatch { fn name() -> &'static HeaderName { &IF_NONE_MATCH } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { Ok(IfNoneMatch(decode_etaglist(values)?)) } fn encode(&self, values: &mut E) where E: Extend, { encode_etaglist(&self.0, values) } } #[derive(Debug, Clone, PartialEq)] pub enum XUpdateRange { FromTo(u64, u64), AllFrom(u64), Last(u64), Append, } impl Header for XUpdateRange { fn name() -> &'static HeaderName { &X_UPDATE_RANGE } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { let mut s = one(values)?.to_str().map_err(map_invalid)?; if s == "append" { return Ok(XUpdateRange::Append); } if !s.starts_with("bytes=") { return Err(invalid()); } s = &s[6..]; let nums = s.split('-').collect::>(); if nums.len() != 2 { return Err(invalid()); } if !nums[0].is_empty() && !nums[1].is_empty() { return Ok(XUpdateRange::FromTo( (nums[0]).parse::().map_err(map_invalid)?, (nums[1]).parse::().map_err(map_invalid)?, )); } if !nums[0].is_empty() { return Ok(XUpdateRange::AllFrom( (nums[0]).parse::().map_err(map_invalid)?, )); } if !nums[1].is_empty() { return Ok(XUpdateRange::Last( (nums[1]).parse::().map_err(map_invalid)?, )); } Err(invalid()) } fn encode(&self, values: &mut E) where E: Extend, { let value = match *self { XUpdateRange::Append => "append".to_string(), XUpdateRange::FromTo(b, e) => format!("{b}-{e}"), XUpdateRange::AllFrom(b) => format!("{b}-"), XUpdateRange::Last(e) => format!("-{e}"), }; values.extend(std::iter::once(HeaderValue::from_str(&value).unwrap())); } } // The "If" header contains IfLists, of which the results are ORed. #[derive(Debug, Clone, PartialEq)] pub struct If(pub Vec); // An IfList contains Conditions, of which the results are ANDed. #[derive(Debug, Clone, PartialEq)] pub struct IfList { pub resource_tag: Option, pub conditions: Vec, } // helpers. impl IfList { fn new() -> IfList { IfList { resource_tag: None, conditions: Vec::new(), } } fn add(&mut self, not: bool, item: IfItem) { self.conditions.push(IfCondition { not, item }); } } // Single Condition is [NOT] State-Token | ETag #[derive(Debug, Clone, PartialEq)] pub struct IfCondition { pub not: bool, pub item: IfItem, } #[derive(Debug, Clone, PartialEq)] pub enum IfItem { StateToken(String), ETag(ETag), } // Below stuff is for the parser state. #[derive(Debug, Clone, PartialEq)] enum IfToken { ListOpen, ListClose, Not, Word(String), Pointy(String), ETag(ETag), End, } #[derive(Debug, Clone, PartialEq)] enum IfState { Start, RTag, List, Not, Bad, } // helpers. fn is_whitespace(c: u8) -> bool { b" \t\r\n".contains(&c) } fn is_special(c: u8) -> bool { b"<>()[]".contains(&c) } fn trim_left(mut out: &'_ [u8]) -> &'_ [u8] { while !out.is_empty() && is_whitespace(out[0]) { out = &out[1..]; } out } // parse one token. fn scan_until(buf: &[u8], c: u8) -> Result<(&[u8], &[u8]), headers::Error> { let mut i = 1; let mut quote = false; while quote || buf[i] != c { if buf.is_empty() || is_whitespace(buf[i]) { return Err(invalid()); } if buf[i] == b'"' { quote = !quote; } i += 1 } Ok((&buf[1..i], &buf[i + 1..])) } // scan one word. fn scan_word(buf: &[u8]) -> Result<(&[u8], &[u8]), headers::Error> { for (i, &c) in buf.iter().enumerate() { if is_whitespace(c) || is_special(c) || c < 32 { if i == 0 { return Err(invalid()); } return Ok((&buf[..i], &buf[i..])); } } Ok((buf, b"")) } // get next token. fn get_token(buf: &'_ [u8]) -> Result<(IfToken, &'_ [u8]), headers::Error> { let buf = trim_left(buf); if buf.is_empty() { return Ok((IfToken::End, buf)); } match buf[0] { b'(' => Ok((IfToken::ListOpen, &buf[1..])), b')' => Ok((IfToken::ListClose, &buf[1..])), b'N' if buf.starts_with(b"Not") => Ok((IfToken::Not, &buf[3..])), b'<' => { let (tok, rest) = scan_until(buf, b'>')?; let s = std::string::String::from_utf8(tok.to_vec()).map_err(map_invalid)?; Ok((IfToken::Pointy(s), rest)) } b'[' => { let (tok, rest) = scan_until(buf, b']')?; let s = std::str::from_utf8(tok).map_err(map_invalid)?; Ok((IfToken::ETag(ETag::from_str(s)?), rest)) } _ => { let (tok, rest) = scan_word(buf)?; if tok == b"Not" { Ok((IfToken::Not, rest)) } else { let s = std::string::String::from_utf8(tok.to_vec()).map_err(map_invalid)?; Ok((IfToken::Word(s), rest)) } } } } impl Header for If { fn name() -> &'static HeaderName { &IF } fn decode<'i, I>(values: &mut I) -> Result where I: Iterator, { // one big state machine. let mut if_lists = If(Vec::new()); let mut cur_list = IfList::new(); let mut state = IfState::Start; let mut input = one(values)?.as_bytes(); loop { let (tok, rest) = get_token(input)?; input = rest; state = match state { IfState::Start => match tok { IfToken::ListOpen => IfState::List, IfToken::Pointy(url) => { let u = url::Url::parse(&url).map_err(map_invalid)?; cur_list.resource_tag = Some(u); IfState::RTag } IfToken::End => { if !if_lists.0.is_empty() { break; } IfState::Bad } _ => IfState::Bad, }, IfState::RTag => match tok { IfToken::ListOpen => IfState::List, _ => IfState::Bad, }, IfState::List | IfState::Not => { let not = state == IfState::Not; match tok { IfToken::Not => { if not { IfState::Bad } else { IfState::Not } } IfToken::Pointy(stok) | IfToken::Word(stok) => { // as we don't have an URI parser, just // check if there's at least one ':' in there. if !stok.contains(':') { IfState::Bad } else { cur_list.add(not, IfItem::StateToken(stok)); IfState::List } } IfToken::ETag(etag) => { cur_list.add(not, IfItem::ETag(etag)); IfState::List } IfToken::ListClose => { if cur_list.conditions.is_empty() { IfState::Bad } else { if_lists.0.push(cur_list); cur_list = IfList::new(); IfState::Start } } _ => IfState::Bad, } } IfState::Bad => return Err(invalid()), }; } Ok(if_lists) } fn encode(&self, values: &mut E) where E: Extend, { let value = "[If header]"; values.extend(std::iter::once(HeaderValue::from_static(value))); } } #[cfg(test)] mod tests { use super::*; #[test] fn if_header() { // Note that some implementations (golang net/x/webdav) also // accept a "plain word" as StateToken, instead of only // a Coded-Url (<...>). We allow that as well, but I have // no idea if we need to (or should!). //let val = r#" ([W/"etag"] Not ) // (Not[W/"bla"] plain:word:123) "#; let val = r#" ([W/"etag"] Not ) (Not[W/"bla"] plain:word:123) "#; let hdrval = HeaderValue::from_static(val); let mut iter = std::iter::once(&hdrval); let hdr = If::decode(&mut iter); assert!(hdr.is_ok()); } #[test] fn etag_header() { let t1 = ETag::from_str(r#"W/"12345""#).unwrap(); let t2 = ETag::from_str(r#"W/"12345""#).unwrap(); let t3 = ETag::from_str(r#""12346""#).unwrap(); let t4 = ETag::from_str(r#""12346""#).unwrap(); assert!(t1 != t2); assert!(t2 != t3); assert!(t3 == t4); } } dav-server-0.11.0/src/davpath.rs000064400000000000000000000301311046102023000145430ustar 00000000000000//! Utility module to handle the path part of an URL as a filesytem path. //! use std::error::Error; use std::ffi::OsStr; #[cfg(target_os = "windows")] use std::ffi::OsString; #[cfg(target_family = "unix")] use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use mime_guess; use percent_encoding as pct; use crate::DavError; // Encode all non-unreserved characters, except '/'. // See RFC3986, and https://en.wikipedia.org/wiki/Percent-encoding . const PATH_ENCODE_SET: &pct::AsciiSet = &pct::NON_ALPHANUMERIC .remove(b'-') .remove(b'_') .remove(b'.') .remove(b'~') .remove(b'/'); /// URL path, with hidden prefix. #[derive(Clone)] pub struct DavPath { fullpath: Vec, pfxlen: Option, } /// Reference to DavPath, no prefix. /// It's what you get when you `Deref` `DavPath`, and returned by `DavPath::with_prefix()`. pub struct DavPathRef { fullpath: [u8], } impl std::fmt::Display for DavPath { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.as_pathbuf().display()) } } impl std::fmt::Debug for DavPath { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{:?}", &self.as_url_string_with_prefix_debug()) } } /// Error returned by some of the DavPath methods. #[derive(Debug)] pub enum ParseError { /// cannot parse InvalidPath, /// outside of prefix PrefixMismatch, /// too many dotdots ForbiddenPath, } impl Error for ParseError { fn description(&self) -> &str { "DavPath parse error" } fn cause(&self) -> Option<&dyn Error> { None } } impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{self:?}") } } impl From for DavError { fn from(e: ParseError) -> Self { match e { ParseError::InvalidPath => DavError::InvalidPath, ParseError::PrefixMismatch => DavError::IllegalPath, ParseError::ForbiddenPath => DavError::ForbiddenPath, } } } // encode path segment with user-defined ENCODE_SET fn encode_path(src: &[u8]) -> Vec { pct::percent_encode(src, PATH_ENCODE_SET) .to_string() .into_bytes() } // make path safe: // - raw path before decoding can contain only printable ascii // - make sure path is absolute // - remove query part (everything after ?) // - merge consecutive slashes // - decode percent encoded bytes, fail on invalid encodings. // - process . and .. // - do not allow NUL or '/' in segments. fn normalize_path(rp: &[u8]) -> Result, ParseError> { // must consist of printable ASCII if rp.iter().any(|&x| !(32..=126).contains(&x)) { return Err(ParseError::InvalidPath); } // don't allow fragments. query part gets deleted. let mut rawpath = rp; if let Some(pos) = rawpath.iter().position(|&x| x == b'?' || x == b'#') { if rawpath[pos] == b'#' { return Err(ParseError::InvalidPath); } rawpath = &rawpath[..pos]; } // must start with "/" if rawpath.is_empty() || rawpath[0] != b'/' { return Err(ParseError::InvalidPath); } // split up in segments let isdir = matches!(rawpath.last(), Some(x) if *x == b'/'); let segments: Vec> = rawpath .split(|c| *c == b'/') .map(|segment| pct::percent_decode(segment).collect()) .collect(); let mut v: Vec<&[u8]> = Vec::new(); for segment in &segments { match &segment[..] { b"." | b"" => {} b".." => { if v.len() < 2 { return Err(ParseError::ForbiddenPath); } v.pop(); v.pop(); } s => { // a decoded segment can contain any value except '/' or '\0' if s.iter().any(|x| *x == 0 || *x == b'/') { return Err(ParseError::InvalidPath); } v.push(b"/"); v.push(s); } } } if isdir || v.is_empty() { v.push(b"/"); } Ok(v.join(&b""[..])) } /// Comparison ignores any trailing slash, so /foo == /foo/ impl PartialEq for DavPath { fn eq(&self, rhs: &DavPath) -> bool { let mut a = self.fullpath.as_slice(); if a.len() > 1 && a.ends_with(b"/") { a = &a[..a.len() - 1]; } let mut b = rhs.fullpath.as_slice(); if b.len() > 1 && b.ends_with(b"/") { b = &b[..b.len() - 1]; } a == b } } impl DavPath { /// from URL encoded path pub fn new(src: &str) -> Result { let path = normalize_path(src.as_bytes())?; Ok(DavPath { fullpath: path.to_vec(), pfxlen: None, }) } /// Set prefix. pub fn set_prefix(&mut self, prefix: &str) -> Result<(), ParseError> { let path = &mut self.fullpath; let prefix = prefix.as_bytes(); if !path.starts_with(prefix) { return Err(ParseError::PrefixMismatch); } let mut pfxlen = prefix.len(); if prefix.ends_with(b"/") { pfxlen -= 1; if path[pfxlen] != b'/' { return Err(ParseError::PrefixMismatch); } } else if path.len() == pfxlen { path.push(b'/'); } self.pfxlen = Some(pfxlen); Ok(()) } /// Return a DavPathRef that refers to the entire URL path with prefix. pub fn with_prefix(&self) -> &DavPathRef { DavPathRef::new(&self.fullpath) } /// from URL encoded path and non-encoded prefix. pub(crate) fn from_str_and_prefix(src: &str, prefix: &str) -> Result { let path = normalize_path(src.as_bytes())?; let mut davpath = DavPath { fullpath: path.to_vec(), pfxlen: None, }; davpath.set_prefix(prefix)?; Ok(davpath) } /// from request.uri pub(crate) fn from_uri_and_prefix( uri: &http::uri::Uri, prefix: &str, ) -> Result { match uri.path() { "*" => Ok(DavPath { fullpath: b"*".to_vec(), pfxlen: None, }), path if path.starts_with('/') => DavPath::from_str_and_prefix(path, prefix), _ => Err(ParseError::InvalidPath), } } /// from request.uri pub fn from_uri(uri: &http::uri::Uri) -> Result { Ok(DavPath { fullpath: uri.path().as_bytes().to_vec(), pfxlen: None, }) } /// add a slash to the end of the path (if not already present). pub(crate) fn add_slash(&mut self) { if !self.is_collection() { self.fullpath.push(b'/'); } } // add a slash pub(crate) fn add_slash_if(&mut self, b: bool) { if b && !self.is_collection() { self.fullpath.push(b'/'); } } /// Add a segment to the end of the path. pub(crate) fn push_segment(&mut self, b: &[u8]) { if !self.is_collection() { self.fullpath.push(b'/'); } self.fullpath.extend_from_slice(b); } // as URL encoded string, with prefix. pub(crate) fn as_url_string_with_prefix_debug(&self) -> String { let mut p = encode_path(self.get_path()); if !self.get_prefix().is_empty() { let mut u = encode_path(self.get_prefix()); u.extend_from_slice(b"["); u.extend_from_slice(&p); u.extend_from_slice(b"]"); p = u; } std::string::String::from_utf8(p).unwrap() } // Return the prefix. fn get_prefix(&self) -> &[u8] { &self.fullpath[..self.pfxlen.unwrap_or(0)] } /// return the URL prefix. pub fn prefix(&self) -> &str { std::str::from_utf8(self.get_prefix()).unwrap() } /// Return the parent directory. pub fn parent(&self) -> DavPath { let mut segs = self .fullpath .split(|&c| c == b'/') .filter(|e| !e.is_empty()) .collect::>(); segs.pop(); if !segs.is_empty() { segs.push(b""); } segs.insert(0, b""); DavPath { pfxlen: self.pfxlen, fullpath: segs.join(&b'/').to_vec(), } } } impl std::ops::Deref for DavPath { type Target = DavPathRef; fn deref(&self) -> &DavPathRef { let pfxlen = self.pfxlen.unwrap_or(0); DavPathRef::new(&self.fullpath[pfxlen..]) } } impl DavPathRef { // NOTE: this is safe, it is what libstd does in std::path::Path::new(), see // https://github.com/rust-lang/rust/blob/6700e186883a83008963d1fdba23eff2b1713e56/src/libstd/path.rs#L1788 fn new(path: &[u8]) -> &DavPathRef { unsafe { &*(path as *const [u8] as *const DavPathRef) } } /// as raw bytes, not encoded, no prefix. pub fn as_bytes(&self) -> &[u8] { self.get_path() } /// as OS specific Path. never ends in "/". pub fn as_pathbuf(&self) -> PathBuf { let mut b = self.get_path(); if b.len() > 1 && b.ends_with(b"/") { b = &b[..b.len() - 1]; } #[cfg(not(target_os = "windows"))] let os_string = OsStr::from_bytes(b).to_owned(); #[cfg(target_os = "windows")] let os_string = OsString::from(String::from_utf8(b.to_vec()).unwrap()); PathBuf::from(os_string) } /// as URL encoded string, with prefix. pub fn as_url_string(&self) -> String { let p = encode_path(self.get_path()); std::string::String::from_utf8(p).unwrap() } /// is this a collection i.e. does the original URL path end in "/". pub fn is_collection(&self) -> bool { self.get_path().ends_with(b"/") } // non-public functions // // Return the path. fn get_path(&self) -> &[u8] { &self.fullpath } // is this a "star" request (only used with OPTIONS) pub(crate) fn is_star(&self) -> bool { self.get_path() == b"*" } /// as OS specific Path, relative (remove first slash) /// /// Used to `push()` onto a pathbuf. pub fn as_rel_ospath(&self) -> &Path { let spath = self.get_path(); let mut path = if !spath.is_empty() { &spath[1..] } else { spath }; if path.ends_with(b"/") { path = &path[..path.len() - 1]; } #[cfg(not(target_os = "windows"))] let os_string = OsStr::from_bytes(path); #[cfg(target_os = "windows")] let os_string: &OsStr = std::str::from_utf8(path).unwrap().as_ref(); Path::new(os_string) } // get parent. #[allow(dead_code)] pub fn parent(&self) -> &DavPathRef { let path = self.get_path(); let mut end = path.len(); while end > 0 { end -= 1; if path[end] == b'/' { if end == 0 { end = 1; } break; } } DavPathRef::new(&path[..end]) } /// The filename is the last segment of the path. Can be empty. pub fn file_name_bytes(&self) -> &[u8] { let segs = self .get_path() .split(|&c| c == b'/') .filter(|e| !e.is_empty()) .collect::>(); if !segs.is_empty() { segs[segs.len() - 1] } else { b"" } } /// The filename is the last segment of the path. Can be empty. pub fn file_name(&self) -> Option<&str> { let name = self.file_name_bytes(); if name.is_empty() { None } else { std::str::from_utf8(name).ok() } } pub(crate) fn get_mime_type_str(&self) -> &'static str { let name = self.file_name_bytes(); let d = name.rsplitn(2, |&c| c == b'.').collect::>(); if d.len() > 1 && let Ok(ext) = std::str::from_utf8(d[0]) && let Some(t) = mime_guess::from_ext(ext).first_raw() { return t; } "application/octet-stream" } } dav-server-0.11.0/src/errors.rs000064400000000000000000000135051046102023000144360ustar 00000000000000use std::error::Error; use std::io::{self, ErrorKind}; use http::StatusCode; use crate::fs::FsError; pub(crate) type DavResult = Result; #[derive(Debug)] pub(crate) enum DavError { XmlReadError, // error reading/parsing xml XmlParseError, // error interpreting xml InvalidPath, // error parsing path IllegalPath, // path not valid here ForbiddenPath, // too many dotdots UnknownDavMethod, ChanError, Utf8Error, Status(StatusCode), StatusClose(StatusCode), FsError(FsError), IoError(io::Error), XmlReaderError(xml::reader::Error), XmlWriterError(xml::writer::Error), } impl Error for DavError { fn description(&self) -> &str { "DAV error" } fn cause(&self) -> Option<&dyn Error> { match *self { DavError::FsError(ref e) => Some(e), DavError::IoError(ref e) => Some(e), DavError::XmlReaderError(ref e) => Some(e), DavError::XmlWriterError(ref e) => Some(e), _ => None, } } } impl std::fmt::Display for DavError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { DavError::XmlReaderError(_) => write!(f, "XML parse error"), DavError::XmlWriterError(_) => write!(f, "XML generate error"), DavError::IoError(_) => write!(f, "I/O error"), _ => write!(f, "{self:?}"), } } } impl From for DavError { fn from(e: FsError) -> Self { DavError::FsError(e) } } impl From for io::Error { fn from(e: DavError) -> Self { match e { DavError::IoError(e) => e, DavError::FsError(e) => e.into(), _ => io::Error::other(e), } } } impl From for io::Error { fn from(e: FsError) -> Self { fserror_to_ioerror(e) } } impl From for DavError { fn from(e: io::Error) -> Self { DavError::IoError(e) } } impl From for DavError { fn from(e: StatusCode) -> Self { DavError::Status(e) } } impl From for DavError { fn from(e: xml::reader::Error) -> Self { DavError::XmlReaderError(e) } } impl From for DavError { fn from(e: xml::writer::Error) -> Self { DavError::XmlWriterError(e) } } impl From for DavError { fn from(_: std::str::Utf8Error) -> Self { DavError::Utf8Error } } impl From for DavError { fn from(_: std::string::FromUtf8Error) -> Self { DavError::Utf8Error } } impl From for DavError { fn from(_e: futures_channel::mpsc::SendError) -> Self { DavError::ChanError } } fn fserror_to_ioerror(e: FsError) -> io::Error { match e { FsError::NotImplemented => io::Error::other("NotImplemented"), FsError::GeneralFailure => io::Error::other("GeneralFailure"), FsError::Exists => io::Error::new(io::ErrorKind::AlreadyExists, "Exists"), FsError::NotFound => io::Error::new(io::ErrorKind::NotFound, "Notfound"), FsError::Forbidden => io::Error::new(io::ErrorKind::PermissionDenied, "Forbidden"), FsError::InsufficientStorage => io::Error::other("InsufficientStorage"), FsError::LoopDetected => io::Error::other("LoopDetected"), FsError::PathTooLong => io::Error::other("PathTooLong"), FsError::TooLarge => io::Error::other("TooLarge"), FsError::IsRemote => io::Error::other("IsRemote"), } } fn ioerror_to_status(ioerror: &io::Error) -> StatusCode { match ioerror.kind() { ErrorKind::NotFound => StatusCode::NOT_FOUND, ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, ErrorKind::AlreadyExists => StatusCode::CONFLICT, ErrorKind::TimedOut => StatusCode::GATEWAY_TIMEOUT, _ => StatusCode::BAD_GATEWAY, } } fn fserror_to_status(e: &FsError) -> StatusCode { match e { FsError::NotImplemented => StatusCode::NOT_IMPLEMENTED, FsError::GeneralFailure => StatusCode::INTERNAL_SERVER_ERROR, FsError::Exists => StatusCode::METHOD_NOT_ALLOWED, FsError::NotFound => StatusCode::NOT_FOUND, FsError::Forbidden => StatusCode::FORBIDDEN, FsError::InsufficientStorage => StatusCode::INSUFFICIENT_STORAGE, FsError::LoopDetected => StatusCode::LOOP_DETECTED, FsError::PathTooLong => StatusCode::URI_TOO_LONG, FsError::TooLarge => StatusCode::PAYLOAD_TOO_LARGE, FsError::IsRemote => StatusCode::BAD_GATEWAY, } } impl DavError { pub(crate) fn statuscode(&self) -> StatusCode { match *self { DavError::XmlReadError => StatusCode::BAD_REQUEST, DavError::XmlParseError => StatusCode::BAD_REQUEST, DavError::InvalidPath => StatusCode::BAD_REQUEST, DavError::IllegalPath => StatusCode::BAD_GATEWAY, DavError::ForbiddenPath => StatusCode::FORBIDDEN, DavError::UnknownDavMethod => StatusCode::NOT_IMPLEMENTED, DavError::ChanError => StatusCode::INTERNAL_SERVER_ERROR, DavError::Utf8Error => StatusCode::UNSUPPORTED_MEDIA_TYPE, DavError::IoError(ref e) => ioerror_to_status(e), DavError::FsError(ref e) => fserror_to_status(e), DavError::Status(e) => e, DavError::StatusClose(e) => e, DavError::XmlReaderError(ref _e) => StatusCode::BAD_REQUEST, DavError::XmlWriterError(ref _e) => StatusCode::INTERNAL_SERVER_ERROR, } } pub(crate) fn must_close(&self) -> bool { !matches!( self, &DavError::Status(_) | &DavError::FsError(FsError::NotFound) | &DavError::FsError(FsError::Forbidden) | &DavError::FsError(FsError::Exists) ) } } dav-server-0.11.0/src/fakels.rs000064400000000000000000000073121046102023000143660ustar 00000000000000//! Fake locksystem (to make Windows/macOS work). //! //! Several Webdav clients, like the ones on Windows and macOS, require just //! basic functionality to mount the Webdav server in read-only mode. However //! to be able to mount the Webdav server in read-write mode, they require the //! Webdav server to have Webdav class 2 compliance - that means, LOCK/UNLOCK //! support. //! //! In many cases, this is not actually important. A lot of the current Webdav //! server implementations that are used to serve a filesystem just fake it: //! LOCK/UNLOCK always succeed, checking for locktokens in //! If: headers always succeeds, and nothing is every really locked. //! //! `FakeLs` implements such a fake locksystem. use std::future; use std::time::{Duration, SystemTime}; use futures_util::FutureExt; use uuid::Uuid; use xmltree::Element; use crate::davpath::DavPath; use crate::ls::*; /// Fake locksystem implementation. #[derive(Debug, Clone)] pub struct FakeLs {} impl FakeLs { /// Create a new "fakels" locksystem. pub fn new() -> Box { Box::new(FakeLs {}) } } fn tm_limit(d: Option) -> Duration { match d { None => Duration::new(120, 0), Some(d) => { if d.as_secs() > 120 { Duration::new(120, 0) } else { d } } } } impl DavLockSystem for FakeLs { fn lock( &'_ self, path: &DavPath, principal: Option<&str>, owner: Option<&Element>, timeout: Option, shared: bool, deep: bool, ) -> LsFuture<'_, Result> { let timeout = tm_limit(timeout); let timeout_at = SystemTime::now() + timeout; let d = if deep { 'I' } else { '0' }; let s = if shared { 'S' } else { 'E' }; let token = format!("opaquetoken:{}/{}/{}", Uuid::new_v4().hyphenated(), d, s); let lock = DavLock { token, path: Box::new(path.clone()), principal: principal.map(|s| s.to_string()), owner: owner.map(|o| Box::new(o.clone())), timeout_at: Some(timeout_at), timeout: Some(timeout), shared, deep, }; debug!("lock {} created", &lock.token); future::ready(Ok(lock)).boxed() } fn unlock(&'_ self, _path: &DavPath, _token: &str) -> LsFuture<'_, Result<(), ()>> { future::ready(Ok(())).boxed() } fn refresh( &'_ self, path: &DavPath, token: &str, timeout: Option, ) -> LsFuture<'_, Result> { debug!("refresh lock {token}"); let v: Vec<&str> = token.split('/').collect(); let deep = v.len() > 1 && v[1] == "I"; let shared = v.len() > 2 && v[2] == "S"; let timeout = tm_limit(timeout); let timeout_at = SystemTime::now() + timeout; let lock = DavLock { token: token.to_string(), path: Box::new(path.clone()), principal: None, owner: None, timeout_at: Some(timeout_at), timeout: Some(timeout), shared, deep, }; future::ready(Ok(lock)).boxed() } fn check( &'_ self, _path: &DavPath, _principal: Option<&str>, _ignore_principal: bool, _deep: bool, _submitted_tokens: &[String], ) -> LsFuture<'_, Result<(), DavLock>> { future::ready(Ok(())).boxed() } fn discover(&'_ self, _path: &DavPath) -> LsFuture<'_, Vec> { future::ready(Vec::new()).boxed() } fn delete(&'_ self, _path: &DavPath) -> LsFuture<'_, Result<(), ()>> { future::ready(Ok(())).boxed() } } dav-server-0.11.0/src/fs.rs000064400000000000000000000640471046102023000135410ustar 00000000000000//! Contains the structs and traits that define a filesystem backend. //! //! You only need this if you are going to implement your own //! filesystem backend. Otherwise, just use 'LocalFs' or 'MemFs'. //! use std::fmt::Debug; use std::future::{self, Future}; use std::io::SeekFrom; use std::pin::Pin; use std::time::{SystemTime, UNIX_EPOCH}; use dyn_clone::{DynClone, clone_trait_object}; use futures_util::{FutureExt, Stream, TryFutureExt}; use http::StatusCode; use crate::davpath::DavPath; macro_rules! notimplemented { ($method:expr_2021) => { Err(FsError::NotImplemented) }; } macro_rules! notimplemented_fut { ($method:expr_2021) => { Box::pin(future::ready(Err(FsError::NotImplemented))) }; } /// Errors generated by a filesystem implementation. /// /// These are more result-codes than errors, really. #[derive(Debug, Clone, Copy, PartialEq)] pub enum FsError { /// Operation not implemented (501) NotImplemented, /// Something went wrong (500) GeneralFailure, /// tried to create something, but it existed (405 / 412) (yes, 405. RFC4918 says so) Exists, /// File / Directory not found (404) NotFound, /// Not allowed (403) Forbidden, /// Out of space (507) InsufficientStorage, /// Symbolic link loop detected (ELOOP) (508) LoopDetected, /// The path is too long (ENAMETOOLONG) (414) PathTooLong, /// The file being PUT is too large (413) TooLarge, /// Trying to MOVE over a mount boundary (EXDEV) (502) IsRemote, } /// The Result type. pub type FsResult = std::result::Result; #[cfg(any(feature = "memfs", feature = "localfs"))] impl From<&std::io::Error> for FsError { fn from(e: &std::io::Error) -> Self { use std::io::ErrorKind; if let Some(errno) = e.raw_os_error() { // specific errors. match errno { #[cfg(unix)] libc::EMLINK | libc::ENOSPC | libc::EDQUOT => return FsError::InsufficientStorage, #[cfg(windows)] libc::EMLINK | libc::ENOSPC => return FsError::InsufficientStorage, libc::EFBIG => return FsError::TooLarge, libc::EACCES | libc::EPERM => return FsError::Forbidden, libc::ENOTEMPTY | libc::EEXIST => return FsError::Exists, libc::ELOOP => return FsError::LoopDetected, libc::ENAMETOOLONG => return FsError::PathTooLong, libc::ENOTDIR => return FsError::Forbidden, libc::EISDIR => return FsError::Forbidden, libc::EROFS => return FsError::Forbidden, libc::ENOENT => return FsError::NotFound, libc::ENOSYS => return FsError::NotImplemented, libc::EXDEV => return FsError::IsRemote, _ => {} } } else { // not an OS error - must be "not implemented" // (e.g. metadata().created() on systems without st_crtime) return FsError::NotImplemented; } // generic mappings for-whatever is left. match e.kind() { ErrorKind::NotFound => FsError::NotFound, ErrorKind::PermissionDenied => FsError::Forbidden, _ => FsError::GeneralFailure, } } } #[cfg(any(feature = "memfs", feature = "localfs"))] impl From for FsError { fn from(e: std::io::Error) -> Self { (&e).into() } } /// A webdav property. #[derive(Debug, Clone)] pub struct DavProp { /// Name of the property. pub name: String, /// XML prefix. pub prefix: Option, /// XML namespace. pub namespace: Option, /// Value of the property as raw XML. Use DavProp::new() to create your custom props pub xml: Option>, } impl DavProp { /// Create XML property with name, prefix, namespace and value pub fn new(name: String, prefix: String, namespace: String, value: String) -> DavProp { DavProp { name: name.clone(), prefix: Some(prefix.clone()), namespace: Some(namespace.clone()), xml: Some( format!( "<{prefix}:{name} xmlns:{prefix}=\"{namespace}\">{value}" ) .into_bytes(), ), } } } /// Future returned by almost all of the DavFileSystem methods. pub type FsFuture<'a, T> = Pin> + Send + 'a>>; /// Convenience alias for a boxed Stream. pub type FsStream = Pin> + Send>>; /// Used as argument to the read_dir() method. /// It is: /// /// - an optimization hint (the implementation may call metadata() and /// store the result in the returned directory entry) /// - a way to get metadata instead of symlink_metadata from /// the directory entry. /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReadDirMeta { /// DavDirEntry.metadata() behaves as metadata() Data, /// DavDirEntry.metadata() behaves as symlink_metadata() DataSymlink, /// No optimizations, otherwise like DataSymlink. None, } /// File system without access control. pub trait DavFileSystem { /// Open a file. fn open<'a>( &'a self, path: &'a DavPath, options: OpenOptions, ) -> FsFuture<'a, Box>; /// Lists entries within a directory. fn read_dir<'a>( &'a self, path: &'a DavPath, meta: ReadDirMeta, ) -> FsFuture<'a, FsStream>>; /// Return the metadata of a file or directory. fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box>; /// Return the metadata of a file, directory or symbolic link. /// /// Differs from [`metadata`][Self::metadata] that if the path is a symbolic link, /// it return the metadata for the link itself, not for the thing /// it points to. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn symlink_metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box> { self.metadata(path) } /// Create a directory. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> { notimplemented_fut!("create_dir") } /// Remove a directory. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> { notimplemented_fut!("remove_dir") } /// Remove a file. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> { notimplemented_fut!("remove_file") } /// Rename a file or directory. /// /// Source and destination must be the same type (file/dir). /// If the destination already exists and is a file, it /// should be replaced. If it is a directory it should give /// an error. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> { notimplemented_fut!("rename") } /// Copy a file. /// /// Should also copy the DAV properties, if properties /// are implemented. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> { notimplemented_fut!("copy") } /// Set the access time of a file / directory. /// /// The default implementation returns [`FsError::NotImplemented`]. #[doc(hidden)] #[allow(unused_variables)] fn set_accessed<'a>(&'a self, path: &'a DavPath, tm: SystemTime) -> FsFuture<'a, ()> { notimplemented_fut!("set_accessed") } /// Set the modified time of a file / directory. /// /// The default implementation returns [`FsError::NotImplemented`]. #[doc(hidden)] #[allow(unused_variables)] fn set_modified<'a>(&'a self, path: &'a DavPath, tm: SystemTime) -> FsFuture<'a, ()> { notimplemented_fut!("set_modified") } /// Indicator that tells if this filesystem driver supports DAV properties. /// /// The default implementation returns `false`. #[allow(unused_variables)] fn have_props<'a>( &'a self, path: &'a DavPath, ) -> Pin + Send + 'a>> { Box::pin(future::ready(false)) } /// Patch the DAV properties of a node (add/remove props). /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn patch_props<'a>( &'a self, path: &'a DavPath, patch: Vec<(bool, DavProp)>, ) -> FsFuture<'a, Vec<(StatusCode, DavProp)>> { notimplemented_fut!("patch_props") } /// List/get the DAV properties of a node. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn get_props<'a>(&'a self, path: &'a DavPath, do_content: bool) -> FsFuture<'a, Vec> { notimplemented_fut!("get_props") } /// Get one specific named property of a node. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn get_prop<'a>(&'a self, path: &'a DavPath, prop: DavProp) -> FsFuture<'a, Vec> { notimplemented_fut!("get_prop") } /// Get quota of this filesystem (used/total space). /// /// The first value returned is the amount of space used, /// the second optional value is the total amount of space /// (used + available). /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn get_quota(&'_ self) -> FsFuture<'_, (u64, Option)> { notimplemented_fut!("get_quota") } } /// File system with access control. Type parameter `C` (credentials) represents /// the authentication and authorization information required for accessing the file system. /// This can include various forms of credentials such as: /// /// - auth tokens, /// - login and password hash combinations, /// - encryption keys. /// /// Additionally, it may encapsulate context, scope or metadata related to the request, /// such as: /// /// - a set of file system permissions granted to a user, /// - regionally-defined access scope. /// /// All [HTTP security considerations][HTTP security] also [apply][WebDAV security] /// to WebDAV, so you should follow the same authorization best practices as you would /// for regular HTTP server. /// /// If credentials `C` include a username, you may want /// to [specify it as principal][crate::DavConfig::principal] /// to the [`DavHandler`][crate::DavHandler] builder so that the locks user requests /// have a known owner. /// /// For file systems without access control, implement simpler [`DavFileSystem`] trait, /// for which there's a blanket implementation of `GuardedFileSystem<()>`. /// /// See also the [auth example]. /// /// [HTTP security]: https://datatracker.ietf.org/doc/html/rfc2616#section-15 /// [WebDAV security]: https://datatracker.ietf.org/doc/html/rfc4918#section-20 /// [auth example]: https://github.com/messense/dav-server-rs/blob/main/examples/auth.rs pub trait GuardedFileSystem: Send + Sync + DynClone where C: Clone + Send + Sync + 'static, { /// Open a file. fn open<'a>( &'a self, path: &'a DavPath, options: OpenOptions, credentials: &'a C, ) -> FsFuture<'a, Box>; /// Lists entries within a directory. fn read_dir<'a>( &'a self, path: &'a DavPath, meta: ReadDirMeta, credentials: &'a C, ) -> FsFuture<'a, FsStream>>; /// Return the metadata of a file or directory. fn metadata<'a>( &'a self, path: &'a DavPath, credentials: &'a C, ) -> FsFuture<'a, Box>; /// Return the metadata of a file, directory or symbolic link. /// /// Differs from [`metadata`][Self::metadata] that if the path is a symbolic link, /// it return the metadata for the link itself, not for the thing /// it points to. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn symlink_metadata<'a>( &'a self, path: &'a DavPath, credentials: &'a C, ) -> FsFuture<'a, Box> { self.metadata(path, credentials) } /// Create a directory. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn create_dir<'a>(&'a self, path: &'a DavPath, credentials: &'a C) -> FsFuture<'a, ()> { notimplemented_fut!("create_dir") } /// Remove a directory. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn remove_dir<'a>(&'a self, path: &'a DavPath, credentials: &'a C) -> FsFuture<'a, ()> { notimplemented_fut!("remove_dir") } /// Remove a file. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn remove_file<'a>(&'a self, path: &'a DavPath, credentials: &'a C) -> FsFuture<'a, ()> { notimplemented_fut!("remove_file") } /// Rename a file or directory. /// /// Source and destination must be the same type (file/dir). /// If the destination already exists and is a file, it /// should be replaced. If it is a directory it should give /// an error. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn rename<'a>( &'a self, from: &'a DavPath, to: &'a DavPath, credentials: &'a C, ) -> FsFuture<'a, ()> { notimplemented_fut!("rename") } /// Copy a file. /// /// Should also copy the DAV properties, if properties /// are implemented. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn copy<'a>( &'a self, from: &'a DavPath, to: &'a DavPath, credentials: &'a C, ) -> FsFuture<'a, ()> { notimplemented_fut!("copy") } /// Set the access time of a file / directory. /// /// The default implementation returns [`FsError::NotImplemented`]. #[doc(hidden)] #[allow(unused_variables)] fn set_accessed<'a>( &'a self, path: &'a DavPath, tm: SystemTime, credentials: &C, ) -> FsFuture<'a, ()> { notimplemented_fut!("set_accessed") } /// Set the modified time of a file / directory. /// /// The default implementation returns [`FsError::NotImplemented`]. #[doc(hidden)] #[allow(unused_variables)] fn set_modified<'a>( &'a self, path: &'a DavPath, tm: SystemTime, credentials: &'a C, ) -> FsFuture<'a, ()> { notimplemented_fut!("set_mofified") } /// Indicator that tells if this filesystem driver supports DAV properties. /// /// The default implementation returns `false`. #[allow(unused_variables)] fn have_props<'a>( &'a self, path: &'a DavPath, credentials: &'a C, ) -> Pin + Send + 'a>> { Box::pin(future::ready(false)) } /// Patch the DAV properties of a node (add/remove props). /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn patch_props<'a>( &'a self, path: &'a DavPath, patch: Vec<(bool, DavProp)>, credentials: &'a C, ) -> FsFuture<'a, Vec<(StatusCode, DavProp)>> { notimplemented_fut!("patch_props") } /// List/get the DAV properties of a node. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn get_props<'a>( &'a self, path: &'a DavPath, do_content: bool, credentials: &'a C, ) -> FsFuture<'a, Vec> { notimplemented_fut!("get_props") } /// Get one specific named property of a node. /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn get_prop<'a>( &'a self, path: &'a DavPath, prop: DavProp, credentials: &'a C, ) -> FsFuture<'a, Vec> { notimplemented_fut!("get_prop") } /// Get quota of this filesystem (used/total space). /// /// The first value returned is the amount of space used, /// the second optional value is the total amount of space /// (used + available). /// /// The default implementation returns [`FsError::NotImplemented`]. #[allow(unused_variables)] fn get_quota<'a>(&'a self, credentials: &'a C) -> FsFuture<'a, (u64, Option)> { notimplemented_fut!("get_quota") } } clone_trait_object! { GuardedFileSystem} impl GuardedFileSystem<()> for Fs { fn open<'a>( &'a self, path: &'a DavPath, options: OpenOptions, _credentials: &(), ) -> FsFuture<'a, Box> { DavFileSystem::open(self, path, options) } fn read_dir<'a>( &'a self, path: &'a DavPath, meta: ReadDirMeta, _credentials: &(), ) -> FsFuture<'a, FsStream>> { DavFileSystem::read_dir(self, path, meta) } fn metadata<'a>( &'a self, path: &'a DavPath, _credentials: &(), ) -> FsFuture<'a, Box> { DavFileSystem::metadata(self, path) } fn symlink_metadata<'a>( &'a self, path: &'a DavPath, _credentials: &(), ) -> FsFuture<'a, Box> { DavFileSystem::symlink_metadata(self, path) } fn create_dir<'a>(&'a self, path: &'a DavPath, _credentials: &()) -> FsFuture<'a, ()> { DavFileSystem::create_dir(self, path) } fn remove_dir<'a>(&'a self, path: &'a DavPath, _credentials: &()) -> FsFuture<'a, ()> { DavFileSystem::remove_dir(self, path) } fn remove_file<'a>(&'a self, path: &'a DavPath, _credentials: &()) -> FsFuture<'a, ()> { DavFileSystem::remove_file(self, path) } fn rename<'a>( &'a self, from: &'a DavPath, to: &'a DavPath, _credentials: &(), ) -> FsFuture<'a, ()> { DavFileSystem::rename(self, from, to) } fn copy<'a>( &'a self, from: &'a DavPath, to: &'a DavPath, _credentials: &(), ) -> FsFuture<'a, ()> { DavFileSystem::copy(self, from, to) } fn set_accessed<'a>( &'a self, path: &'a DavPath, tm: SystemTime, _credentials: &(), ) -> FsFuture<'a, ()> { DavFileSystem::set_accessed(self, path, tm) } fn set_modified<'a>( &'a self, path: &'a DavPath, tm: SystemTime, _credentials: &(), ) -> FsFuture<'a, ()> { DavFileSystem::set_modified(self, path, tm) } fn have_props<'a>( &'a self, path: &'a DavPath, _credentials: &(), ) -> Pin + Send + 'a>> { DavFileSystem::have_props(self, path) } fn patch_props<'a>( &'a self, path: &'a DavPath, patch: Vec<(bool, DavProp)>, _credentials: &(), ) -> FsFuture<'a, Vec<(StatusCode, DavProp)>> { DavFileSystem::patch_props(self, path, patch) } fn get_props<'a>( &'a self, path: &'a DavPath, do_content: bool, _credentials: &(), ) -> FsFuture<'a, Vec> { DavFileSystem::get_props(self, path, do_content) } fn get_prop<'a>( &'a self, path: &'a DavPath, prop: DavProp, _credentials: &(), ) -> FsFuture<'a, Vec> { DavFileSystem::get_prop(self, path, prop) } fn get_quota(&'_ self, _credentials: &()) -> FsFuture<'_, (u64, Option)> { DavFileSystem::get_quota(self) } } /// One directory entry (or child node). pub trait DavDirEntry: Send + Sync { /// Name of the entry. fn name(&self) -> Vec; /// Metadata of the entry. fn metadata(&'_ self) -> FsFuture<'_, Box>; /// Default implementation of `is_dir` just returns `metadata()?.is_dir()`. /// Implementations can override this if their `metadata()` method is /// expensive and there is a cheaper way to provide the same info /// (e.g. dirent.d_type in unix filesystems). fn is_dir(&'_ self) -> FsFuture<'_, bool> { Box::pin( self.metadata() .and_then(|meta| future::ready(Ok(meta.is_dir()))), ) } /// Likewise. Default: `!is_dir()`. fn is_file(&'_ self) -> FsFuture<'_, bool> { Box::pin( self.metadata() .and_then(|meta| future::ready(Ok(meta.is_file()))), ) } /// Likewise. Default: `false`. fn is_symlink(&'_ self) -> FsFuture<'_, bool> { Box::pin( self.metadata() .and_then(|meta| future::ready(Ok(meta.is_symlink()))), ) } } /// A `DavFile` is the equivalent of `std::fs::File`, should be /// readable/writeable/seekable, and be able to return its metadata. pub trait DavFile: Debug + Send + Sync { fn metadata(&'_ mut self) -> FsFuture<'_, Box>; fn write_buf(&'_ mut self, buf: Box) -> FsFuture<'_, ()>; fn write_bytes(&'_ mut self, buf: bytes::Bytes) -> FsFuture<'_, ()>; fn read_bytes(&'_ mut self, count: usize) -> FsFuture<'_, bytes::Bytes>; fn seek(&'_ mut self, pos: SeekFrom) -> FsFuture<'_, u64>; fn flush(&'_ mut self) -> FsFuture<'_, ()>; fn redirect_url(&'_ mut self) -> FsFuture<'_, Option> { future::ready(Ok(None)).boxed() } } /// File metadata. Basically type, length, and some timestamps. pub trait DavMetaData: Debug + Send + Sync + DynClone { /// Size of the file. fn len(&self) -> u64; /// `Modified` timestamp. fn modified(&self) -> FsResult; /// File or directory (aka collection). fn is_dir(&self) -> bool; /// Is Calendar collection? #[cfg(feature = "caldav")] fn is_calendar(&self, path: &DavPath) -> bool; /// Is Address Book collection? #[cfg(feature = "carddav")] fn is_addressbook(&self, path: &DavPath) -> bool; /// Simplistic implementation of `etag()` /// /// Returns a simple etag that basically is `\-\` /// with the numbers in hex. Enough for most implementations. fn etag(&self) -> Option { if let Ok(t) = self.modified() && let Ok(t) = t.duration_since(UNIX_EPOCH) { let t = t.as_secs() * 1000000 + t.subsec_nanos() as u64 / 1000; let tag = if self.is_file() && self.len() > 0 { format!("{:x}-{:x}", self.len(), t) } else { format!("{t:x}") }; return Some(tag); } None } /// Is this a file and not a directory. Default: `!is_dir()`. fn is_file(&self) -> bool { !self.is_dir() } /// Is this a symbolic link. Default: false. fn is_symlink(&self) -> bool { false } /// Last access time. Default: `FsError::NotImplemented`. fn accessed(&self) -> FsResult { notimplemented!("access time") } /// Creation time. Default: `FsError::NotImplemented`. fn created(&self) -> FsResult { notimplemented!("creation time") } /// Inode change time (ctime). Default: `FsError::NotImplemented`. fn status_changed(&self) -> FsResult { notimplemented!("status change time") } /// Is file executable (unix: has "x" mode bit). Default: `FsError::NotImplemented`. fn executable(&self) -> FsResult { notimplemented!("executable") } // Is empty file fn is_empty(&self) -> bool { self.len() == 0 } } clone_trait_object! {DavMetaData} /// OpenOptions for `open()`. #[derive(Debug, Clone, Default)] pub struct OpenOptions { /// open for reading pub read: bool, /// open for writing pub write: bool, /// open in write-append mode pub append: bool, /// truncate file first when writing pub truncate: bool, /// create file if it doesn't exist pub create: bool, /// must create new file, fail if it already exists. pub create_new: bool, /// write file total size pub size: Option, /// checksum, owncloud extension pub checksum: Option, } impl OpenOptions { #[allow(dead_code)] pub(crate) fn new() -> OpenOptions { OpenOptions { read: false, write: false, append: false, truncate: false, create: false, create_new: false, size: None, checksum: None, } } pub(crate) fn read() -> OpenOptions { OpenOptions { read: true, write: false, append: false, truncate: false, create: false, create_new: false, size: None, checksum: None, } } pub(crate) fn write() -> OpenOptions { OpenOptions { read: false, write: true, append: false, truncate: false, create: false, create_new: false, size: None, checksum: None, } } } impl std::error::Error for FsError { fn description(&self) -> &str { "DavFileSystem error" } fn cause(&self) -> Option<&dyn std::error::Error> { None } } impl std::fmt::Display for FsError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{self:?}") } } dav-server-0.11.0/src/handle_caldav.rs000064400000000000000000000467141046102023000156770ustar 00000000000000use chrono::Utc; use futures_util::StreamExt; use headers::HeaderMapExt; use http::{Request, Response, StatusCode}; use std::io::Cursor; use xml::reader::{EventReader, XmlEvent}; use xmltree::{Element, XMLNode}; use crate::body::Body; use crate::errors::*; use crate::fs::*; use crate::{DavInner, DavResult}; use crate::async_stream::AsyncStream; use crate::caldav::*; use crate::davpath::DavPath; use crate::handle_props::PropWriter; impl DavInner { /// Handle REPORT method for CalDAV and CardDAV /// /// This method detects the namespace of the request body and routes /// to the appropriate CalDAV or CardDAV handler. pub(crate) async fn handle_report( &self, req: &Request<()>, body: &[u8], ) -> DavResult> { // First, check if this is a CardDAV request by looking for CardDAV elements #[cfg(feature = "carddav")] if self.is_carddav_report(body) { return self.handle_carddav_report(req, body).await; } let path = self.path(req); // Parse the REPORT request body as CalDAV let report_type = self.parse_report_request(body)?; match report_type { CalDavReportType::CalendarQuery(query) => { self.handle_calendar_query(&path, query).await } CalDavReportType::CalendarMultiget { hrefs } => { self.handle_calendar_multiget(hrefs).await } CalDavReportType::FreeBusyQuery { time_range } => { self.handle_freebusy_query(&path, time_range).await } } } /// Check if the REPORT request body is a CardDAV request /// /// This parses the XML root element to check if it's a CardDAV request /// by examining the namespace and element name. #[cfg(feature = "carddav")] fn is_carddav_report(&self, body: &[u8]) -> bool { use crate::carddav::NS_CARDDAV_URI; if body.is_empty() { return false; } // Parse just enough to get the root element's name and namespace let cursor = Cursor::new(body); let parser = EventReader::new(cursor); for event in parser { match event { Ok(XmlEvent::StartElement { name, namespace, .. }) => { // Check if this is a CardDAV element by namespace if let Some(prefix) = &name.prefix && let Some(uri) = namespace.get(prefix) && uri == NS_CARDDAV_URI { return true; } // Also check by element name for common CardDAV REPORT types match name.local_name.as_str() { "addressbook-query" | "addressbook-multiget" => return true, _ => return false, } } Err(_) => return false, _ => continue, } } false } /// Handle CalDAV MKCALENDAR method pub(crate) async fn handle_mkcalendar( &self, req: &Request<()>, _body: &[u8], ) -> DavResult> { let path = self.path(req); // Check if the collection already exists if self.fs.metadata(&path, &self.credentials).await.is_ok() { return Err(DavError::StatusClose(StatusCode::METHOD_NOT_ALLOWED)); } // Create the calendar collection self.fs.create_dir(&path, &self.credentials).await?; // Set calendar-specific properties to identify this as a calendar collection // Note: This may fail if the filesystem doesn't support properties, but that's OK // because is_calendar() uses path-based detection as a fallback let _ = self.set_calendar_properties(&path).await; let mut resp = Response::new(Body::empty()); *resp.status_mut() = StatusCode::CREATED; resp.headers_mut().typed_insert(headers::ContentLength(0)); Ok(resp) } fn parse_report_request(&self, body: &[u8]) -> DavResult { if body.is_empty() { return Err(DavError::StatusClose(StatusCode::BAD_REQUEST)); } let cursor = Cursor::new(body); let parser = EventReader::new(cursor); let mut elements: Vec = Vec::new(); let mut current_element: Option = None; let mut element_stack: Vec = Vec::new(); for event in parser { match event { Ok(XmlEvent::StartElement { name, attributes, namespace, }) => { let mut elem = Element::new(&name.local_name); if let Some(prefix) = name.prefix && let Some(uri) = namespace.get(&prefix) { elem.namespace = Some(uri.to_string()); } for attr in attributes { elem.attributes.insert(attr.name.local_name, attr.value); } if let Some(parent) = current_element.take() { element_stack.push(parent); } current_element = Some(elem); } Ok(XmlEvent::EndElement { .. }) => { if let Some(elem) = current_element.take() { if let Some(mut parent) = element_stack.pop() { parent.children.push(XMLNode::Element(elem)); current_element = Some(parent); } else { elements.push(elem); } } } Ok(XmlEvent::Characters(text)) => { if let Some(ref mut elem) = current_element { elem.children.push(XMLNode::Text(text)); } } _ => {} } } // Parse the root element to determine report type if let Some(root) = elements.first() { match root.name.as_str() { "calendar-query" => { let query = self.parse_calendar_query(root)?; Ok(CalDavReportType::CalendarQuery(query)) } "calendar-multiget" => { let hrefs = self.parse_calendar_multiget(root)?; Ok(CalDavReportType::CalendarMultiget { hrefs }) } "free-busy-query" => { let time_range = self.parse_freebusy_query(root)?; Ok(CalDavReportType::FreeBusyQuery { time_range }) } _ => Err(DavError::StatusClose(StatusCode::BAD_REQUEST)), } } else { Err(DavError::StatusClose(StatusCode::BAD_REQUEST)) } } fn parse_calendar_query(&self, root: &Element) -> DavResult { let mut query = CalendarQuery { comp_filter: None, time_range: None, properties: Vec::new(), }; for child in &root.children { if let XMLNode::Element(elem) = child { match elem.name.as_str() { "filter" => { // Parse comp-filter elements for filter_child in &elem.children { if let XMLNode::Element(filter_elem) = filter_child && filter_elem.name == "comp-filter" { query.comp_filter = Some(self.parse_component_filter(filter_elem)?); } } } "prop" => { // Parse requested properties for prop_child in &elem.children { if let XMLNode::Element(prop_elem) = prop_child { query.properties.push(prop_elem.name.clone()); } } } _ => {} } } } Ok(query) } fn parse_component_filter(&self, elem: &Element) -> DavResult { let name = elem .attributes .get("name") .ok_or(DavError::StatusClose(StatusCode::BAD_REQUEST))? .clone(); let mut filter = ComponentFilter { name, is_not_defined: false, time_range: None, prop_filters: Vec::new(), comp_filters: Vec::new(), }; for child in &elem.children { if let XMLNode::Element(child_elem) = child { match child_elem.name.as_str() { "is-not-defined" => { filter.is_not_defined = true; } "time-range" => { filter.time_range = Some(self.parse_time_range(child_elem)?); } "prop-filter" => { filter .prop_filters .push(self.parse_property_filter(child_elem)?); } "comp-filter" => { filter .comp_filters .push(self.parse_component_filter(child_elem)?); } _ => {} } } } Ok(filter) } fn parse_property_filter(&self, elem: &Element) -> DavResult { let name = elem .attributes .get("name") .ok_or(DavError::StatusClose(StatusCode::BAD_REQUEST))? .clone(); let mut filter = PropertyFilter { name, is_not_defined: false, text_match: None, time_range: None, param_filters: Vec::new(), }; for child in &elem.children { if let XMLNode::Element(child_elem) = child { match child_elem.name.as_str() { "is-not-defined" => { filter.is_not_defined = true; } "time-range" => { filter.time_range = Some(self.parse_time_range(child_elem)?); } "text-match" => { filter.text_match = Some(self.parse_text_match(child_elem)?); } _ => {} } } } Ok(filter) } fn parse_time_range(&self, elem: &Element) -> DavResult { Ok(TimeRange { start: elem.attributes.get("start").cloned(), end: elem.attributes.get("end").cloned(), }) } fn parse_text_match(&self, elem: &Element) -> DavResult { let text = elem .children .iter() .find_map(|child| { if let XMLNode::Text(text) = child { Some(text.clone()) } else { None } }) .unwrap_or_default(); Ok(TextMatch { text, collation: elem.attributes.get("collation").cloned(), negate_condition: elem .attributes .get("negate-condition") .map(|v| v == "yes") .unwrap_or(false), match_type: elem.attributes.get("match-type").cloned(), }) } fn parse_calendar_multiget(&self, root: &Element) -> DavResult> { let mut hrefs = Vec::new(); for child in &root.children { if let XMLNode::Element(elem) = child && elem.name == "href" { for href_child in &elem.children { if let XMLNode::Text(href) = href_child { hrefs.push(href.clone()); } } } } Ok(hrefs) } fn parse_freebusy_query(&self, root: &Element) -> DavResult { for child in &root.children { if let XMLNode::Element(elem) = child && elem.name == "time-range" { return self.parse_time_range(elem); } } Err(DavError::StatusClose(StatusCode::BAD_REQUEST)) } async fn handle_calendar_query( &self, path: &DavPath, query: CalendarQuery, ) -> DavResult> { // Get directory listing let stream = self .fs .read_dir(path, ReadDirMeta::Data, &self.credentials) .await?; let mut results = Vec::new(); let items: Vec<_> = stream.collect().await; for item in items { match item { Ok(dirent) => { let mut item_path = path.clone(); item_path.push_segment(&dirent.name()); // Check if this is a calendar resource, and append content to result if let Ok(mut file) = self .fs .open(&item_path, OpenOptions::read(), &self.credentials) .await { let metadata = file.metadata().await?; let etag = metadata.etag().unwrap_or_default().to_string(); if let Ok(data) = file.read_bytes(metadata.len() as usize).await && is_calendar_data(&data) { let content = String::from_utf8_lossy(&data); if self.matches_query(&content, &query) { results.push((item_path.clone(), etag, content.to_string())); continue; } } } } Err(_) => continue, } } // Generate multistatus response self.generate_calendar_multiget_response(results, Vec::new()) .await } async fn handle_calendar_multiget(&self, hrefs: Vec) -> DavResult> { let mut results = Vec::new(); let mut missing_hrefs: Vec = Vec::new(); for href in &hrefs { if let Ok(item_path) = DavPath::from_str_and_prefix(href, &self.prefix) && let Ok(mut file) = self .fs .open(&item_path, OpenOptions::read(), &self.credentials) .await && let Ok(metadata) = file.metadata().await && let Ok(data) = file.read_bytes(metadata.len() as usize).await && is_calendar_data(&data) { let etag = metadata.etag().unwrap_or_default().to_string(); let content = String::from_utf8_lossy(&data); results.push((item_path, etag, content.to_string())); continue; } missing_hrefs.push(href.clone()); } self.generate_calendar_multiget_response(results, missing_hrefs) .await } async fn handle_freebusy_query( &self, _: &DavPath, time_range: TimeRange, ) -> DavResult> { //TODO: freebusy implementation // For now, return an empty freebusy response // A full implementation would analyze calendar events and generate freebusy information let freebusy_data = format!( "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAV-SERVER//CalDAV//EN\r\n\ BEGIN:VFREEBUSY\r\nUID:{}\r\nDTSTAMP:{}Z\r\n\ DTSTART:{}\r\nDTEND:{}\r\n\ END:VFREEBUSY\r\nEND:VCALENDAR\r\n", uuid::Uuid::new_v4(), Utc::now().format("%Y%m%dT%H%M%S"), time_range.start.as_deref().unwrap_or("20000101T000000Z"), time_range.end.as_deref().unwrap_or("20991231T235959Z") ); let mut resp = Response::new(Body::from(freebusy_data)); resp.headers_mut().insert( "content-type", "text/calendar; charset=utf-8".parse().unwrap(), ); Ok(resp) } fn matches_query(&self, content: &str, query: &CalendarQuery) -> bool { // Simple implementation - a full implementation would parse the iCalendar // and apply all the filters properly if let Some(ref comp_filter) = query.comp_filter && !comp_filter.name.is_empty() && !content.contains(&format!("BEGIN:{}", comp_filter.name)) { false } else { true } } #[cfg(feature = "caldav")] async fn generate_calendar_multiget_response( &self, results: Vec<(DavPath, String, String)>, missing_hrefs: Vec, ) -> DavResult> { let mut resp = Response::new(Body::empty()); // Create a minimal request for PropWriter let req = http::Request::builder() .method(http::Method::GET) .uri("/") .body(()) .unwrap(); let empty_path = DavPath::new("/").unwrap(); let mut pw = PropWriter::new( &req, &mut resp, "prop", Vec::new(), self.fs.clone(), self.ls.as_ref(), self.principal.clone(), self.credentials.clone(), &empty_path, )?; *resp.body_mut() = Body::from(AsyncStream::new(|tx| async move { pw.set_tx(tx); for (href, etag, calendar_data) in results { pw.write_calendar_data_response(&href, &etag, &calendar_data)?; } for missing_href in missing_hrefs { pw.write_calendar_not_found_response(&missing_href)?; } pw.close().await?; Ok(()) })); Ok(resp) } /// Save Calendar data to DavFile /// /// Set calendar-specific properties to identify a directory as a calendar collection async fn set_calendar_properties(&self, path: &DavPath) -> DavResult<()> { use crate::fs::DavProp; // Set supported-calendar-component-set property let comp_set_prop = DavProp { name: "supported-calendar-component-set".to_string(), prefix: Some("C".to_string()), namespace: Some(NS_CALDAV_URI.to_string()), xml: Some(b"".to_vec()), }; // Set calendar-description property let desc_prop = DavProp { name: "calendar-description".to_string(), prefix: Some("C".to_string()), namespace: Some(NS_CALDAV_URI.to_string()), xml: Some(b"Calendar Collection".to_vec()), }; // Save properties using patch_props (true = set property) let patch = vec![(true, comp_set_prop), (true, desc_prop)]; self.fs.patch_props(path, patch, &self.credentials).await?; Ok(()) } } dav-server-0.11.0/src/handle_carddav.rs000064400000000000000000000457731046102023000160550ustar 00000000000000use futures_util::StreamExt; use headers::HeaderMapExt; use http::{Request, Response, StatusCode}; use std::io::Cursor; use xml::reader::{EventReader, XmlEvent}; use xmltree::{Element, XMLNode}; use crate::body::Body; use crate::errors::*; use crate::fs::*; use crate::{DavInner, DavResult}; use crate::async_stream::AsyncStream; use crate::carddav::*; use crate::davpath::DavPath; use crate::handle_props::PropWriter; impl DavInner { /// Handle REPORT method when only CardDAV is enabled (not CalDAV) /// /// When CalDAV is also enabled, the unified handle_report in handle_caldav.rs /// is used instead, which delegates to handle_carddav_report for CardDAV requests. #[cfg(not(feature = "caldav"))] pub(crate) async fn handle_report( &self, req: &Request<()>, body: &[u8], ) -> DavResult> { self.handle_carddav_report(req, body).await } /// Handle CardDAV REPORT method for addressbook-query and addressbook-multiget pub(crate) async fn handle_carddav_report( &self, req: &Request<()>, body: &[u8], ) -> DavResult> { let path = self.path(req); // Parse the REPORT request body let report_type = self.parse_carddav_report_request(body)?; match report_type { CardDavReportType::AddressBookQuery(query) => { self.handle_addressbook_query(&path, query).await } CardDavReportType::AddressBookMultiget { hrefs } => { self.handle_addressbook_multiget(hrefs).await } } } /// Handle CardDAV MKADDRESSBOOK method pub(crate) async fn handle_mkaddressbook( &self, req: &Request<()>, _body: &[u8], ) -> DavResult> { let path = self.path(req); // Check if the collection already exists if self.fs.metadata(&path, &self.credentials).await.is_ok() { return Err(DavError::StatusClose(StatusCode::METHOD_NOT_ALLOWED)); } // Create the addressbook collection self.fs.create_dir(&path, &self.credentials).await?; // Set addressbook-specific properties to identify this as an addressbook collection // Note: This may fail if the filesystem doesn't support properties, but that's OK // because is_addressbook() uses path-based detection as a fallback let _ = self.set_addressbook_properties(&path).await; let mut resp = Response::new(Body::empty()); *resp.status_mut() = StatusCode::CREATED; resp.headers_mut().typed_insert(headers::ContentLength(0)); Ok(resp) } fn parse_carddav_report_request(&self, body: &[u8]) -> DavResult { if body.is_empty() { return Err(DavError::StatusClose(StatusCode::BAD_REQUEST)); } let cursor = Cursor::new(body); let parser = EventReader::new(cursor); let mut elements: Vec = Vec::new(); let mut current_element: Option = None; let mut element_stack: Vec = Vec::new(); for event in parser { match event { Ok(XmlEvent::StartElement { name, attributes, namespace, }) => { let mut elem = Element::new(&name.local_name); if let Some(prefix) = name.prefix && let Some(uri) = namespace.get(&prefix) { elem.namespace = Some(uri.to_string()); } for attr in attributes { elem.attributes.insert(attr.name.local_name, attr.value); } if let Some(parent) = current_element.take() { element_stack.push(parent); } current_element = Some(elem); } Ok(XmlEvent::EndElement { .. }) => { if let Some(elem) = current_element.take() { if let Some(mut parent) = element_stack.pop() { parent.children.push(XMLNode::Element(elem)); current_element = Some(parent); } else { elements.push(elem); } } } Ok(XmlEvent::Characters(text)) => { if let Some(ref mut elem) = current_element { elem.children.push(XMLNode::Text(text)); } } _ => {} } } // Parse the root element to determine report type if let Some(root) = elements.first() { match root.name.as_str() { "addressbook-query" => { let query = self.parse_addressbook_query(root)?; Ok(CardDavReportType::AddressBookQuery(query)) } "addressbook-multiget" => { let hrefs = self.parse_addressbook_multiget(root)?; Ok(CardDavReportType::AddressBookMultiget { hrefs }) } _ => Err(DavError::StatusClose(StatusCode::BAD_REQUEST)), } } else { Err(DavError::StatusClose(StatusCode::BAD_REQUEST)) } } fn parse_addressbook_query(&self, root: &Element) -> DavResult { let mut query = AddressBookQuery { prop_filter: None, properties: Vec::new(), limit: None, }; for child in &root.children { if let XMLNode::Element(elem) = child { match elem.name.as_str() { "filter" => { // Parse prop-filter elements for filter_child in &elem.children { if let XMLNode::Element(filter_elem) = filter_child && filter_elem.name == "prop-filter" { query.prop_filter = Some(self.parse_carddav_property_filter(filter_elem)?); } } } "prop" => { // Parse requested properties for prop_child in &elem.children { if let XMLNode::Element(prop_elem) = prop_child { query.properties.push(prop_elem.name.clone()); } } } "limit" => { // Parse limit element for limit_child in &elem.children { if let XMLNode::Element(limit_elem) = limit_child && limit_elem.name == "nresults" && let Some(text) = limit_elem.children.iter().find_map(|c| { if let XMLNode::Text(t) = c { Some(t) } else { None } }) { query.limit = text.parse().ok(); } } } _ => {} } } } Ok(query) } fn parse_carddav_property_filter(&self, elem: &Element) -> DavResult { let name = elem .attributes .get("name") .ok_or(DavError::StatusClose(StatusCode::BAD_REQUEST))? .clone(); let mut filter = PropertyFilter { name, is_not_defined: false, text_match: None, param_filters: Vec::new(), }; for child in &elem.children { if let XMLNode::Element(child_elem) = child { match child_elem.name.as_str() { "is-not-defined" => { filter.is_not_defined = true; } "text-match" => { filter.text_match = Some(self.parse_carddav_text_match(child_elem)?); } "param-filter" => { filter .param_filters .push(self.parse_carddav_param_filter(child_elem)?); } _ => {} } } } Ok(filter) } fn parse_carddav_text_match(&self, elem: &Element) -> DavResult { let text = elem .children .iter() .find_map(|child| { if let XMLNode::Text(text) = child { Some(text.clone()) } else { None } }) .unwrap_or_default(); Ok(TextMatch { text, collation: elem.attributes.get("collation").cloned(), negate_condition: elem .attributes .get("negate-condition") .map(|v| v == "yes") .unwrap_or(false), match_type: elem.attributes.get("match-type").cloned(), }) } fn parse_carddav_param_filter(&self, elem: &Element) -> DavResult { let name = elem .attributes .get("name") .ok_or(DavError::StatusClose(StatusCode::BAD_REQUEST))? .clone(); let mut filter = ParameterFilter { name, is_not_defined: false, text_match: None, }; for child in &elem.children { if let XMLNode::Element(child_elem) = child { match child_elem.name.as_str() { "is-not-defined" => { filter.is_not_defined = true; } "text-match" => { filter.text_match = Some(self.parse_carddav_text_match(child_elem)?); } _ => {} } } } Ok(filter) } fn parse_addressbook_multiget(&self, root: &Element) -> DavResult> { let mut hrefs = Vec::new(); for child in &root.children { if let XMLNode::Element(elem) = child && elem.name == "href" { for href_child in &elem.children { if let XMLNode::Text(href) = href_child { hrefs.push(href.clone()); } } } } Ok(hrefs) } async fn handle_addressbook_query( &self, path: &DavPath, query: AddressBookQuery, ) -> DavResult> { // Get directory listing let stream = self .fs .read_dir(path, ReadDirMeta::Data, &self.credentials) .await?; let mut results = Vec::new(); let items: Vec<_> = stream.collect().await; let mut count = 0u32; for item in items { // Check limit if let Some(limit) = query.limit && count >= limit { break; } match item { Ok(dirent) => { let mut item_path = path.clone(); item_path.push_segment(&dirent.name()); // Check if this is a vCard resource, and append content to result if let Ok(mut file) = self .fs .open(&item_path, OpenOptions::read(), &self.credentials) .await { let metadata = file.metadata().await?; let etag = metadata.etag().unwrap_or_default().to_string(); if let Ok(data) = file.read_bytes(metadata.len() as usize).await && is_vcard_data(&data) { let content = String::from_utf8_lossy(&data); if self.matches_addressbook_query(&content, &query) { results.push((item_path.clone(), etag, content.to_string())); count += 1; continue; } } } } Err(_) => continue, } } // Generate multistatus response self.generate_addressbook_multiget_response(results, Vec::new()) .await } async fn handle_addressbook_multiget(&self, hrefs: Vec) -> DavResult> { let mut results = Vec::new(); let mut missing_hrefs: Vec = Vec::new(); for href in &hrefs { if let Ok(item_path) = DavPath::from_str_and_prefix(href, &self.prefix) && let Ok(mut file) = self .fs .open(&item_path, OpenOptions::read(), &self.credentials) .await && let Ok(metadata) = file.metadata().await && let Ok(data) = file.read_bytes(metadata.len() as usize).await && is_vcard_data(&data) { let etag = metadata.etag().unwrap_or_default().to_string(); let content = String::from_utf8_lossy(&data); results.push((item_path, etag, content.to_string())); continue; } missing_hrefs.push(href.clone()); } self.generate_addressbook_multiget_response(results, missing_hrefs) .await } fn matches_addressbook_query(&self, content: &str, query: &AddressBookQuery) -> bool { // Simple implementation - a full implementation would parse the vCard // and apply all the filters properly if let Some(ref prop_filter) = query.prop_filter { if prop_filter.is_not_defined { // Check if the property is NOT defined let prop_name = format!("{}:", prop_filter.name.to_uppercase()); if content.contains(&prop_name) { return false; } } else if let Some(ref text_match) = prop_filter.text_match { // Check for text match let search_text = if text_match.negate_condition { // Negate condition - return true if text is NOT found !self.text_matches(content, &text_match.text, text_match.match_type.as_deref()) } else { self.text_matches(content, &text_match.text, text_match.match_type.as_deref()) }; return search_text; } } true } fn text_matches(&self, content: &str, search: &str, match_type: Option<&str>) -> bool { let content_lower = content.to_lowercase(); let search_lower = search.to_lowercase(); match match_type { Some("equals") => content_lower.contains(&format!(":{}", search_lower)), Some("starts-with") => { // Check if any property value starts with the search text for line in content.lines() { if let Some(pos) = line.find(':') { let value = &line[pos + 1..]; if value.to_lowercase().starts_with(&search_lower) { return true; } } } false } Some("ends-with") => { // Check if any property value ends with the search text for line in content.lines() { if let Some(pos) = line.find(':') { let value = &line[pos + 1..]; if value.to_lowercase().ends_with(&search_lower) { return true; } } } false } _ => { // Default: contains content_lower.contains(&search_lower) } } } #[cfg(feature = "carddav")] async fn generate_addressbook_multiget_response( &self, results: Vec<(DavPath, String, String)>, missing_hrefs: Vec, ) -> DavResult> { let mut resp = Response::new(Body::empty()); // Create a minimal request for PropWriter let req = http::Request::builder() .method(http::Method::GET) .uri("/") .body(()) .unwrap(); let empty_path = DavPath::new("/").unwrap(); let mut pw = PropWriter::new( &req, &mut resp, "prop", Vec::new(), self.fs.clone(), self.ls.as_ref(), self.principal.clone(), self.credentials.clone(), &empty_path, )?; *resp.body_mut() = Body::from(AsyncStream::new(|tx| async move { pw.set_tx(tx); for (href, etag, vcard_data) in results { pw.write_vcard_data_response(&href, &etag, &vcard_data)?; } for missing_href in missing_hrefs { pw.write_vcard_not_found_response(&missing_href)?; } pw.close().await?; Ok(()) })); Ok(resp) } /// Set addressbook-specific properties to identify a directory as an addressbook collection async fn set_addressbook_properties(&self, path: &DavPath) -> DavResult<()> { use crate::fs::DavProp; // Set supported-address-data property let addr_data_prop = DavProp { name: "supported-address-data".to_string(), prefix: Some("CARD".to_string()), namespace: Some(NS_CARDDAV_URI.to_string()), xml: Some(b"".to_vec()), }; // Set addressbook-description property let desc_prop = DavProp { name: "addressbook-description".to_string(), prefix: Some("CARD".to_string()), namespace: Some(NS_CARDDAV_URI.to_string()), xml: Some(b"Address Book Collection".to_vec()), }; // Save properties using patch_props (true = set property) let patch = vec![(true, addr_data_prop), (true, desc_prop)]; self.fs.patch_props(path, patch, &self.credentials).await?; Ok(()) } } dav-server-0.11.0/src/handle_copymove.rs000064400000000000000000000264331046102023000163020ustar 00000000000000use futures_util::{FutureExt, StreamExt, future::BoxFuture}; use headers::HeaderMapExt; use http::{Request, Response, StatusCode}; use crate::async_stream::AsyncStream; use crate::body::Body; use crate::conditional::*; use crate::davheaders::{self, Depth}; use crate::davpath::DavPath; use crate::errors::*; use crate::fs::*; use crate::multierror::{MultiError, multi_error}; use crate::{DavInner, DavResult, util::DavMethod}; // map_err helper. async fn add_status<'a>( m_err: &'a mut MultiError, path: &'a DavPath, e: impl Into + 'static, ) -> DavResult<()> { let daverror = e.into(); if let Err(x) = m_err.add_status(path, daverror.statuscode()).await { return Err(x.into()); } Err(daverror) } impl DavInner { pub(crate) fn do_copy<'a>( &'a self, source: &'a DavPath, topdest: &'a DavPath, dest: &'a DavPath, depth: Depth, multierror: &'a mut MultiError, ) -> BoxFuture<'a, DavResult<()>> { async move { // when doing "COPY /a/b /a/b/c make sure we don't recursively // copy /a/b/c/ into /a/b/c. if source == topdest { return Ok(()); } // source must exist. let meta = match self.fs.metadata(source, &self.credentials).await { Err(e) => return add_status(multierror, source, e).await, Ok(m) => m, }; // if it's a file we can overwrite it. if !meta.is_dir() { return match self.fs.copy(source, dest, &self.credentials).await { Ok(_) => Ok(()), Err(e) => { debug!("do_copy: self.fs.copy error: {e:?}"); add_status(multierror, source, e).await } }; } // Copying a directory onto an existing directory with Depth 0 // is not an error. It means "only copy properties" (which // we do not do yet). if let Err(e) = self.fs.create_dir(dest, &self.credentials).await && (depth != Depth::Zero || e != FsError::Exists) { debug!("do_copy: self.fs.create_dir({dest}) error: {e:?}"); return add_status(multierror, dest, e).await; } // only recurse when Depth > 0. if depth == Depth::Zero { return Ok(()); } let mut entries = match self .fs .read_dir(source, ReadDirMeta::DataSymlink, &self.credentials) .await { Ok(entries) => entries, Err(e) => { debug!("do_copy: self.fs.read_dir error: {e:?}"); return add_status(multierror, source, e).await; } }; // If we encounter errors, just print them, and keep going. // Last seen error is returned from function. let mut retval = Ok::<_, DavError>(()); while let Some(dirent) = entries.next().await { let dirent = match dirent { Ok(dirent) => dirent, Err(e) => return add_status(multierror, source, e).await, }; // NOTE: dirent.metadata() behaves like symlink_metadata() let meta = match dirent.metadata().await { Ok(meta) => meta, Err(e) => return add_status(multierror, source, e).await, }; let name = dirent.name(); let mut nsrc = source.clone(); let mut ndest = dest.clone(); nsrc.push_segment(&name); ndest.push_segment(&name); if meta.is_dir() { nsrc.add_slash(); ndest.add_slash(); } // recurse. if let Err(e) = self .do_copy(&nsrc, topdest, &ndest, depth, multierror) .await { retval = Err(e); } } retval } .boxed() } // Right now we handle MOVE with a simple RENAME. RFC4918 #9.9.2 talks // about "partially failed moves", which means that we might have to // try to move directories with increasing granularity to move as much // as possible instead of all-or-nothing. // // Note that this might not be optional, as the RFC says: // // "Any headers included with MOVE MUST be applied in processing every // resource to be moved with the exception of the Destination header." // // .. so for perfect compliance we might have to process all resources // one-by-one anyway. But seriously, who cares. // pub(crate) async fn do_move<'a>( &'a self, source: &'a DavPath, dest: &'a DavPath, multierror: &'a mut MultiError, ) -> DavResult<()> { if let Err(e) = self.fs.rename(source, dest, &self.credentials).await { add_status(multierror, source, e).await } else { Ok(()) } } pub(crate) async fn handle_copymove( self, req: &Request<()>, method: DavMethod, ) -> DavResult> { // get and check headers. let overwrite = req .headers() .typed_get::() .is_none_or(|o| o.0); let depth = match req.headers().typed_get::() { Some(Depth::Infinity) | None => Depth::Infinity, Some(Depth::Zero) if method == DavMethod::Copy => Depth::Zero, _ => return Err(StatusCode::BAD_REQUEST.into()), }; // decode and validate destination. let dest = match req.headers().typed_get::() { Some(dest) => DavPath::from_str_and_prefix(&dest.0, &self.prefix)?, None => return Err(StatusCode::BAD_REQUEST.into()), }; // for MOVE, tread with care- if the path ends in "/" but it actually // is a symlink, we want to move the symlink, not what it points to. let mut path = self.path(req); let meta = if method == DavMethod::Move { let meta = self.fs.symlink_metadata(&path, &self.credentials).await?; if meta.is_symlink() { let m2 = self.fs.metadata(&path, &self.credentials).await?; path.add_slash_if(m2.is_dir()); } meta } else { self.fs.metadata(&path, &self.credentials).await? }; path.add_slash_if(meta.is_dir()); // parent of the destination must exist. if !self.has_parent(&dest).await { return Err(StatusCode::CONFLICT.into()); } // for the destination, also check if it's a symlink. If we are going // to remove it first, we want to remove the link, not what it points to. let (dest_is_file, dmeta) = match self.fs.symlink_metadata(&dest, &self.credentials).await { Ok(meta) => { let mut is_file = false; if meta.is_symlink() && let Ok(m) = self.fs.metadata(&dest, &self.credentials).await { is_file = m.is_file(); } if meta.is_file() { is_file = true; } (is_file, Ok(meta)) } Err(e) => (false, Err(e)), }; // check if overwrite is "F" let exists = dmeta.is_ok(); if !overwrite && exists { return Err(StatusCode::PRECONDITION_FAILED.into()); } // check if source == dest if path == dest { return Err(StatusCode::FORBIDDEN.into()); } // check If and If-* headers for source URL let tokens = match if_match_get_tokens( req, Some(meta.as_ref()), self.fs.as_ref(), &self.ls, &path, &self.credentials, ) .await { Ok(t) => t, Err(s) => return Err(s.into()), }; // check locks. since we cancel the entire operation if there is // a conflicting lock, we do not return a 207 multistatus, but // just a simple status. if let Some(ref locksystem) = self.ls { let principal = self.principal.as_deref(); if method == DavMethod::Move { // for MOVE check if source path is locked if let Err(_l) = locksystem .check(&path, principal, false, true, &tokens) .await { return Err(StatusCode::LOCKED.into()); } } // for MOVE and COPY check if destination is locked if let Err(_l) = locksystem .check(&dest, principal, false, true, &tokens) .await { return Err(StatusCode::LOCKED.into()); } } let req_path = path.clone(); let items = AsyncStream::new(|tx| { async move { let mut multierror = MultiError::new(tx); // see if we need to delete the destination first. if overwrite && exists && (depth != Depth::Zero || dest_is_file) { trace!("handle_copymove: deleting destination {dest}"); if self .delete_items(&mut multierror, Depth::Infinity, dmeta.unwrap(), &dest) .await .is_err() { return Ok(()); } // should really do this per item, in case the delete partially fails. See TODO.md if let Some(ref locksystem) = self.ls { let _ = locksystem.delete(&dest).await; } } // COPY or MOVE. if method == DavMethod::Copy { if self .do_copy(&path, &dest, &dest, depth, &mut multierror) .await .is_ok() { let s = if exists { StatusCode::NO_CONTENT } else { StatusCode::CREATED }; let _ = multierror.add_status(&path, s).await; } } else { // move and if successful, remove locks at old location. if self.do_move(&path, &dest, &mut multierror).await.is_ok() { if let Some(ref locksystem) = self.ls { locksystem.delete(&path).await.ok(); } let s = if exists { StatusCode::NO_CONTENT } else { StatusCode::CREATED }; let _ = multierror.add_status(&path, s).await; } } Ok::<_, DavError>(()) } }); multi_error(req_path, items).await } } dav-server-0.11.0/src/handle_delete.rs000064400000000000000000000152031046102023000156740ustar 00000000000000use futures_util::{FutureExt, StreamExt, future::BoxFuture}; use headers::HeaderMapExt; use http::{Request, Response, StatusCode}; use crate::async_stream::AsyncStream; use crate::body::Body; use crate::conditional::if_match_get_tokens; use crate::davheaders::Depth; use crate::davpath::DavPath; use crate::errors::*; use crate::fs::*; use crate::multierror::{MultiError, multi_error}; use crate::{DavInner, DavResult}; // map_err helper. async fn add_status<'a>(m_err: &'a mut MultiError, path: &'a DavPath, e: FsError) -> DavError { let status = DavError::FsError(e).statuscode(); if let Err(x) = m_err.add_status(path, status).await { return x.into(); } DavError::Status(status) } // map_err helper for directories, the result statuscode // mappings are not 100% the same. async fn dir_status<'a>(res: &'a mut MultiError, path: &'a DavPath, e: FsError) -> DavError { let status = match e { FsError::Exists => StatusCode::CONFLICT, e => DavError::FsError(e).statuscode(), }; if let Err(x) = res.add_status(path, status).await { return x.into(); } DavError::Status(status) } impl DavInner { pub(crate) fn delete_items<'a>( &'a self, res: &'a mut MultiError, depth: Depth, meta: Box, path: &'a DavPath, ) -> BoxFuture<'a, DavResult<()>> { async move { if !meta.is_dir() { trace!("delete_items (file) {path} {depth:?}"); return match self.fs.remove_file(path, &self.credentials).await { Ok(x) => Ok(x), Err(e) => Err(add_status(res, path, e).await), }; } if depth == Depth::Zero { trace!("delete_items (dir) {path} {depth:?}"); return match self.fs.remove_dir(path, &self.credentials).await { Ok(x) => Ok(x), Err(e) => Err(add_status(res, path, e).await), }; } // walk over all entries. let mut entries = match self .fs .read_dir(path, ReadDirMeta::DataSymlink, &self.credentials) .await { Ok(x) => Ok(x), Err(e) => Err(add_status(res, path, e).await), }?; let mut result = Ok(()); while let Some(dirent) = entries.next().await { let dirent = match dirent { Ok(dirent) => dirent, Err(e) => { result = Err(add_status(res, path, e).await); continue; } }; // if metadata() fails, skip to next entry. // NOTE: dirent.metadata == symlink_metadata (!) let meta = match dirent.metadata().await { Ok(m) => m, Err(e) => { result = Err(add_status(res, path, e).await); continue; } }; let mut npath = path.clone(); npath.push_segment(&dirent.name()); npath.add_slash_if(meta.is_dir()); // do the actual work. If this fails with a non-fs related error, // return immediately. if let Err(e) = self.delete_items(res, depth, meta, &npath).await { match e { DavError::Status(_) => { result = Err(e); continue; } _ => return Err(e), } } } // if we got any error, return with the error, // and do not try to remove the directory. result?; match self.fs.remove_dir(path, &self.credentials).await { Ok(x) => Ok(x), Err(e) => Err(dir_status(res, path, e).await), } } .boxed() } pub(crate) async fn handle_delete(self, req: &Request<()>) -> DavResult> { // RFC4918 9.6.1 DELETE for Collections. // Note that allowing Depth: 0 is NOT RFC compliant. let depth = match req.headers().typed_get::() { Some(Depth::Infinity) | None => Depth::Infinity, Some(Depth::Zero) => Depth::Zero, _ => return Err(DavError::Status(StatusCode::BAD_REQUEST)), }; let mut path = self.path(req); let meta = self.fs.symlink_metadata(&path, &self.credentials).await?; if meta.is_symlink() && let Ok(m2) = self.fs.metadata(&path, &self.credentials).await { path.add_slash_if(m2.is_dir()); } path.add_slash_if(meta.is_dir()); // check the If and If-* headers. let tokens_res = if_match_get_tokens( req, Some(meta.as_ref()), self.fs.as_ref(), &self.ls, &path, &self.credentials, ) .await; let tokens = match tokens_res { Ok(t) => t, Err(s) => return Err(DavError::Status(s)), }; // check locks. since we cancel the entire operation if there is // a conflicting lock, we do not return a 207 multistatus, but // just a simple status. if let Some(ref locksystem) = self.ls { let principal = self.principal.as_deref(); if let Err(_l) = locksystem .check(&path, principal, false, true, &tokens) .await { return Err(DavError::Status(StatusCode::LOCKED)); } } let req_path = path.clone(); let items = AsyncStream::new(|tx| { async move { // turn the Sink into something easier to pass around. let mut multierror = MultiError::new(tx); // now delete the path recursively. let fut = self.delete_items(&mut multierror, depth, meta, &path); if let Ok(()) = fut.await { // Done. Now delete the path in the locksystem as well. // Should really do this per resource, in case the delete partially fails. See TODO.pm if let Some(ref locksystem) = self.ls { locksystem.delete(&path).await.ok(); } let _ = multierror.add_status(&path, StatusCode::NO_CONTENT).await; } Ok(()) } }); multi_error(req_path, items).await } } dav-server-0.11.0/src/handle_gethead.rs000064400000000000000000000450041046102023000160350ustar 00000000000000use std::cmp; use std::io::Write; use chrono::{DateTime, Utc}; use futures_util::StreamExt; use headers::HeaderMapExt; use http::{Request, Response, status::StatusCode}; use bytes::Bytes; use crate::async_stream::AsyncStream; use crate::body::Body; use crate::conditional; use crate::davheaders; use crate::davpath::DavPath; use crate::errors::*; use crate::fs::*; use crate::{DavInner, DavMethod}; struct Range { start: u64, count: u64, } const BOUNDARY: &str = "BOUNDARY"; const BOUNDARY_START: &str = "\n--BOUNDARY\n"; const BOUNDARY_END: &str = "\n--BOUNDARY--\n"; const READ_BUF_SIZE: usize = 16384; impl DavInner { pub(crate) async fn handle_get(&self, req: &Request<()>) -> DavResult> { let head = req.method() == http::Method::HEAD; let mut path = self.path(req); // GNOME Online Accounts handler for Nextcloud sends GET /remote.php/webdav/ request only to test its connection and expects 200 OK with empty body if !self.autoindex.unwrap_or(false) && !head && path.as_bytes() == b"/remote.php/webdav/" { let mut response = Response::new(Body::empty()); let headers = response.headers_mut(); headers.insert("Content-Length", "0".parse().unwrap()); headers.insert("Accept-Ranges", "bytes".parse().unwrap()); return Ok(response); } // check if it's a directory. let meta = self.fs.metadata(&path, &self.credentials).await?; if meta.is_dir() { // // This is a directory. If the path doesn't end in "/", send a redir. // Most webdav clients handle redirect really bad, but a client asking // for a directory index is usually a browser. // if !path.is_collection() { let mut res = Response::new(Body::empty()); path.add_slash(); res.headers_mut().insert( "Location", path.with_prefix().as_url_string().parse().unwrap(), ); res.headers_mut().typed_insert(headers::ContentLength(0)); *res.status_mut() = StatusCode::FOUND; return Ok(res); } // If indexfile was set, use it. if let Some(indexfile) = self.indexfile.as_ref() { path.push_segment(indexfile.as_bytes()); } else { // Otherwise see if we need to generate a directory index. return self.handle_autoindex(req, head).await; } } // double check, is it a regular file. let mut file = self .fs .open(&path, OpenOptions::read(), &self.credentials) .await?; #[allow(unused_mut)] let mut meta = file.metadata().await?; if !meta.is_file() { return Err(DavError::Status(StatusCode::METHOD_NOT_ALLOWED)); } let len = meta.len(); let mut curpos = 0u64; let file_etag = davheaders::ETag::from_meta(meta.as_ref()); let mut ranges = Vec::new(); let mut do_range = match req.headers().typed_try_get::() { Ok(Some(r)) => conditional::ifrange_match(&r, file_etag.as_ref(), meta.modified().ok()), Ok(None) => true, Err(_) => false, }; let mut res = Response::new(Body::empty()); let mut no_body = false; // set Last-Modified and ETag headers. if let Ok(modified) = meta.modified() { res.headers_mut() .typed_insert(headers::LastModified::from(modified)); } if let Some(etag) = file_etag { res.headers_mut().typed_insert(etag); } if let Some(redirect) = self.redirect && redirect && let Some(url) = file.redirect_url().await? { res.headers_mut().insert("Location", url.parse().unwrap()); *res.status_mut() = StatusCode::FOUND; return Ok(res); } // Apache always adds an Accept-Ranges header, even with partial // responses where it should be pretty obvious. So something somewhere // probably depends on that. res.headers_mut() .typed_insert(headers::AcceptRanges::bytes()); // handle the if-headers. if let Some(s) = conditional::if_match( req, Some(meta.as_ref()), self.fs.as_ref(), &self.ls, &path, &self.credentials, ) .await { *res.status_mut() = s; no_body = true; do_range = false; } // see if we want to get one or more ranges. if do_range && let Some(r) = req.headers().typed_get::() { trace!("handle_gethead: range header {r:?}"); use std::ops::Bound::*; for range in r.satisfiable_ranges(len) { let (start, mut count, valid) = match range { (Included(s), Included(e)) if e >= s => (s, e - s + 1, true), (Included(s), Unbounded) if s <= len => (s, len - s, true), (Unbounded, Included(n)) if n <= len => (len - n, n, true), _ => (0, 0, false), }; if !valid || start >= len { let r = format!("bytes */{len}"); res.headers_mut() .insert("Content-Range", r.parse().unwrap()); *res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; ranges.clear(); no_body = true; break; } if start + count > len { count = len - start; } ranges.push(Range { start, count }); } } if !ranges.is_empty() { // seek to beginning of the first range. if file .seek(std::io::SeekFrom::Start(ranges[0].start)) .await .is_err() { let r = format!("bytes */{len}"); res.headers_mut() .insert("Content-Range", r.parse().unwrap()); *res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; ranges.clear(); no_body = true; } } if !ranges.is_empty() { curpos = ranges[0].start; *res.status_mut() = StatusCode::PARTIAL_CONTENT; if ranges.len() == 1 { // add content-range header. let r = format!( "bytes {}-{}/{}", ranges[0].start, ranges[0].start + ranges[0].count - 1, len ); res.headers_mut() .insert("Content-Range", r.parse().unwrap()); } else { // add content-type header. let r = format!("multipart/byteranges; boundary={BOUNDARY}"); res.headers_mut().insert("Content-Type", r.parse().unwrap()); } } else { // normal request, send entire file. ranges.push(Range { start: 0, count: len, }); } // set content-length and start if we're not doing multipart. let content_type = path.get_mime_type_str(); if ranges.len() <= 1 { res.headers_mut() .typed_insert(davheaders::ContentType(content_type.to_owned())); let notmod = res.status() == StatusCode::NOT_MODIFIED; let len = if head || !no_body || notmod { ranges[0].count } else { 0 }; res.headers_mut().typed_insert(headers::ContentLength(len)); } if head || no_body { return Ok(res); } // now just loop and send data. let read_buf_size = self.read_buf_size.unwrap_or(READ_BUF_SIZE); *res.body_mut() = Body::from(AsyncStream::new(|mut tx| { async move { let zero = [0; 4096]; let multipart = ranges.len() > 1; for range in ranges { trace!( "handle_get: start = {}, count = {}", range.start, range.count ); if curpos != range.start { // this should never fail, but if it does, just skip this range // and try the next one. if let Err(_e) = file.seek(std::io::SeekFrom::Start(range.start)).await { debug!("handle_get: failed to seek to {}: {:?}", range.start, _e); continue; } curpos = range.start; } if multipart { let mut hdrs = Vec::new(); let _ = write!(hdrs, "{BOUNDARY_START}"); let _ = writeln!( hdrs, "Content-Range: bytes {}-{}/{}", range.start, range.start + range.count - 1, len ); let _ = writeln!(hdrs, "Content-Type: {content_type}"); let _ = writeln!(hdrs); tx.send(Bytes::from(hdrs)).await; } let mut count = range.count; while count > 0 { let blen = cmp::min(count, read_buf_size as u64) as usize; let mut buf = file.read_bytes(blen).await?; if buf.is_empty() { // this is a cop out. if the file got truncated, just // return zeroed bytes instead of file content. let n = if count > 4096 { 4096 } else { count as usize }; buf = Bytes::copy_from_slice(&zero[..n]); } let len = buf.len() as u64; count = count.saturating_sub(len); curpos += len; trace!("sending {len} bytes"); tx.send(buf).await; } } if multipart { tx.send(Bytes::from(BOUNDARY_END)).await; } Ok::<(), std::io::Error>(()) } })); Ok(res) } pub(crate) async fn handle_autoindex( &self, req: &Request<()>, head: bool, ) -> DavResult> { let mut res = Response::new(Body::empty()); let path = self.path(req); // Is PROPFIND explicitly allowed? let allow_propfind = self .allow .map(|x| x.contains(DavMethod::PropFind)) .unwrap_or(false); // Only allow index generation if explicitly set to true, _or_ if it was // unset, and PROPFIND is explicitly allowed. if !self.autoindex.unwrap_or(allow_propfind) { debug!( "method {} not allowed on request {}", req.method(), req.uri() ); return Err(DavError::StatusClose(StatusCode::METHOD_NOT_ALLOWED)); } // read directory or bail. let mut entries = self .fs .read_dir(&path, ReadDirMeta::Data, &self.credentials) .await?; // start output res.headers_mut() .insert("Content-Type", "text/html; charset=utf-8".parse().unwrap()); *res.status_mut() = StatusCode::OK; if head { return Ok(res); } // now just loop and send data. *res.body_mut() = Body::from(AsyncStream::new(|mut tx| { async move { // transform all entries into a dirent struct. struct Dirent { path: String, name: String, meta: Box, } let mut dirents: Vec = Vec::new(); while let Some(dirent) = entries.next().await { let dirent = match dirent { Ok(dirent) => dirent, Err(e) => { trace!("next dir entry error happened. Skipping {e:?}"); continue; } }; let mut name = dirent.name(); if name.starts_with(b".") { continue; } let mut npath = path.clone(); npath.push_segment(&name); if let Ok(meta) = dirent.metadata().await { if meta.is_dir() { name.push(b'/'); npath.add_slash(); } dirents.push(Dirent { path: npath.with_prefix().as_url_string(), name: String::from_utf8_lossy(&name).to_string(), meta, }); } } // now we can sort the dirent struct. dirents.sort_by(|a, b| { let adir = a.meta.is_dir(); let bdir = b.meta.is_dir(); if adir && !bdir { std::cmp::Ordering::Less } else if bdir && !adir { std::cmp::Ordering::Greater } else { (a.name).cmp(&b.name) } }); // and output html let upath = htmlescape::encode_minimal(&path.with_prefix().as_url_string()); let mut w = String::new(); w.push_str( "\ \n\ \n\ \n\ Index of ", ); w.push_str(&upath); w.push_str("\n"); w.push_str( "\ \n\ \n\ \n", ); w.push_str(&format!("

Index of {}

", display_path(&path))); w.push_str( "\ \n\ \n\ \n\ \n\ \n\ \n\ \n\ \n\ \n\ \n\ \n\ \n", ); tx.send(Bytes::from(w)).await; for dirent in &dirents { let modified = match dirent.meta.modified() { Ok(t) => DateTime::::from(t) .format("%Y-%m-%d %H:%M") .to_string(), Err(_) => "".to_string(), }; let size = match dirent.meta.is_file() { true => display_size(dirent.meta.len()), false => "[DIR] ".to_string(), }; let name = htmlescape::encode_minimal(&dirent.name); let s = format!( "", dirent.path, name, modified, size ); tx.send(Bytes::from(s)).await; } let mut w = String::new(); w.push_str(""); w.push_str("
NameLast modifiedSize

Parent Directory [DIR]
{}{}{}

"); tx.send(Bytes::from(w)).await; Ok::<_, std::io::Error>(()) } })); Ok(res) } } fn display_size(size: u64) -> String { let (formatted, unit) = ["KiB", "MiB", "GiB", "TiB", "PiB"] .iter() .zip(1..) .find(|(_, power)| size >= 1024u64.pow(*power) && size < 1024u64.pow(power + 1)) .map(|(unit, power)| (size as f64 / 1024u64.pow(power) as f64, unit)) .unwrap_or_else(|| (size as f64, &"B")); format!("{} {}", (formatted * 100f64).round() / 100f64, unit) } fn display_path(path: &DavPath) -> String { let path_dsp = String::from_utf8_lossy(path.with_prefix().as_bytes()); let path_url = path.with_prefix().as_url_string(); let dpath_segs = path_dsp .split('/') .filter(|s| !s.is_empty()) .collect::>(); let upath_segs = path_url .split('/') .filter(|s| !s.is_empty()) .collect::>(); let mut dpath = String::new(); let mut upath = String::new(); if dpath_segs.is_empty() { dpath.push('/'); } else { dpath.push_str("/"); } for idx in 0..dpath_segs.len() { upath.push('/'); upath.push_str(upath_segs[idx]); let dseg = htmlescape::encode_minimal(dpath_segs[idx]); if idx == dpath_segs.len() - 1 { dpath.push_str(&dseg); } else { dpath.push_str(&format!("{dseg}/")); } } dpath } dav-server-0.11.0/src/handle_lock.rs000064400000000000000000000271261046102023000153710ustar 00000000000000use headers::HeaderMapExt; use http::StatusCode as SC; use http::{Request, Response}; use std::cmp; use std::io::Cursor; use std::time::Duration; use xmltree::{self, Element}; use crate::body::Body; use crate::conditional::{dav_if_match, if_match}; use crate::davheaders::{self, DavTimeout}; use crate::davpath::DavPath; use crate::errors::*; use crate::fs::{FsError, OpenOptions}; use crate::ls::*; use crate::util::MemBuffer; use crate::xmltree_ext::{self, ElementExt}; use crate::{DavInner, DavResult}; impl DavInner { pub(crate) async fn handle_lock( &self, req: &Request<()>, xmldata: &[u8], ) -> DavResult> { // must have a locksystem or bail let locksystem = match self.ls { Some(ref ls) => ls, None => return Err(SC::METHOD_NOT_ALLOWED.into()), }; let mut res = Response::new(Body::empty()); // path and meta let mut path = self.path(req); let meta = match self.fs.metadata(&path, &self.credentials).await { Ok(meta) => Some(self.fixpath(&mut res, &mut path, meta)), Err(_) => None, }; // lock refresh? if xmldata.is_empty() { // get locktoken let (_, tokens) = dav_if_match(req, self.fs.as_ref(), &self.ls, &path, &self.credentials).await; if tokens.len() != 1 { return Err(SC::BAD_REQUEST.into()); } // try refresh if locksystem .check(&path, self.principal.as_deref(), false, false, &tokens) .await .is_ok() { let timeout = get_timeout(req, true, false); let lock = match locksystem.refresh(&path, &tokens[0], timeout).await { Ok(lock) => lock, Err(_) => return Err(SC::PRECONDITION_FAILED.into()), }; // output result let prop = build_lock_prop(&lock, true); let mut emitter = xmltree_ext::emitter(MemBuffer::new())?; prop.write_ev(&mut emitter)?; let buffer = emitter.into_inner().take(); let ct = "application/xml; charset=utf-8".to_owned(); res.headers_mut().typed_insert(davheaders::ContentType(ct)); *res.body_mut() = Body::from(buffer); return Ok(res); } else { return Err(SC::PRECONDITION_FAILED.into()); } } // handle Depth: let deep = match req.headers().typed_get::() { Some(davheaders::Depth::Infinity) | None => true, Some(davheaders::Depth::Zero) => false, _ => return Err(SC::BAD_REQUEST.into()), }; // handle the if-headers. if let Some(s) = if_match( req, meta.as_ref().map(|v| v.as_ref()), self.fs.as_ref(), &self.ls, &path, &self.credentials, ) .await { return Err(s.into()); } // Cut & paste from method_put.rs .... let mut oo = OpenOptions::write(); oo.create = true; if req .headers() .typed_get::() .is_some_and(|h| h.0 == davheaders::ETagList::Star) { oo.create = false; } if req .headers() .typed_get::() .is_some_and(|h| h.0 == davheaders::ETagList::Star) { oo.create_new = true; } // parse xml let tree = xmltree::Element::parse2(Cursor::new(xmldata))?; if tree.name != "lockinfo" { return Err(DavError::XmlParseError); } // decode Element. let mut shared: Option = None; let mut owner: Option = None; let mut locktype = false; for elem in tree.child_elems_iter() { match elem.name.as_str() { "lockscope" => { let name = elem.child_elems_iter().map(|e| e.name.as_ref()).next(); match name { Some("exclusive") => shared = Some(false), Some("shared") => shared = Some(true), _ => return Err(DavError::XmlParseError), } } "locktype" => { let name = elem.child_elems_iter().map(|e| e.name.as_ref()).next(); match name { Some("write") => locktype = true, _ => return Err(DavError::XmlParseError), } } "owner" => { let mut o = elem.clone(); o.prefix = Some("D".to_owned()); owner = Some(o); } _ => return Err(DavError::XmlParseError), } } // sanity check. if shared.is_none() || !locktype { return Err(DavError::XmlParseError); }; let shared = shared.unwrap(); // create lock let timeout = get_timeout(req, false, shared); let principal = self.principal.as_deref(); let lock = match locksystem .lock(&path, principal, owner.as_ref(), timeout, shared, deep) .await { Ok(lock) => lock, Err(_) => return Err(SC::LOCKED.into()), }; // try to create file if it doesn't exist. let create = oo.create; let create_new = oo.create_new; if meta.is_none() { match self.fs.open(&path, oo, &self.credentials).await { Ok(_) => {} Err(FsError::NotFound) | Err(FsError::Exists) => { let s = if !create || create_new { SC::PRECONDITION_FAILED } else { SC::CONFLICT }; let _ = locksystem.unlock(&path, &lock.token).await; return Err(s.into()); } Err(e) => { let _ = locksystem.unlock(&path, &lock.token).await; return Err(e.into()); } }; } // output result let lt = format!("<{}>", lock.token); let ct = "application/xml; charset=utf-8".to_owned(); res.headers_mut().typed_insert(davheaders::LockToken(lt)); res.headers_mut().typed_insert(davheaders::ContentType(ct)); if meta.is_none() { *res.status_mut() = SC::CREATED; } else { *res.status_mut() = SC::OK; } let mut emitter = xmltree_ext::emitter(MemBuffer::new())?; let prop = build_lock_prop(&lock, true); prop.write_ev(&mut emitter)?; let buffer = emitter.into_inner().take(); *res.body_mut() = Body::from(buffer); Ok(res) } pub(crate) async fn handle_unlock(&self, req: &Request<()>) -> DavResult> { // must have a locksystem or bail let locksystem = match self.ls { Some(ref ls) => ls, None => return Err(SC::METHOD_NOT_ALLOWED.into()), }; // Must have Lock-Token header let t = req .headers() .typed_get::() .ok_or(DavError::Status(SC::BAD_REQUEST))?; let token = t.0.trim_matches(|c| c == '<' || c == '>'); let mut res = Response::new(Body::empty()); let mut path = self.path(req); if let Ok(meta) = self.fs.metadata(&path, &self.credentials).await { self.fixpath(&mut res, &mut path, meta); } match locksystem.unlock(&path, token).await { Ok(_) => { *res.status_mut() = SC::NO_CONTENT; Ok(res) } Err(_) => Err(SC::CONFLICT.into()), } } } #[allow(clippy::borrowed_box)] pub(crate) async fn list_lockdiscovery( ls: Option<&Box>, path: &DavPath, ) -> Element { let mut elem = Element::new2("D:lockdiscovery"); // must have a locksystem or bail let locksystem = match ls { Some(ls) => ls, None => return elem, }; // list the locks. let locks = locksystem.discover(path).await; for lock in &locks { elem.push_element(build_lock_prop(lock, false)); } elem } #[allow(clippy::borrowed_box)] pub(crate) fn list_supportedlock(ls: Option<&Box>) -> Element { let mut elem = Element::new2("D:supportedlock"); // must have a locksystem or bail if ls.is_none() { return elem; } let mut entry = Element::new2("D:lockentry"); let mut scope = Element::new2("D:lockscope"); let mut ltype = Element::new2("D:locktype"); scope.push_element(Element::new2("D:exclusive")); ltype.push_element(Element::new2("D:write")); entry.push_element(scope); entry.push_element(ltype); elem.push_element(entry); let mut entry = Element::new2("D:lockentry"); let mut scope = Element::new2("D:lockscope"); let mut ltype = Element::new2("D:locktype"); scope.push_element(Element::new2("D:shared")); ltype.push_element(Element::new2("D:write")); entry.push_element(scope); entry.push_element(ltype); elem.push_element(entry); elem } // process timeout header fn get_timeout(req: &Request<()>, refresh: bool, shared: bool) -> Option { let max_timeout = if shared { Duration::new(86400, 0) } else { Duration::new(600, 0) }; match req.headers().typed_get::() { Some(davheaders::Timeout(ref vec)) if !vec.is_empty() => match vec[0] { DavTimeout::Infinite => { if refresh { None } else { Some(max_timeout) } } DavTimeout::Seconds(n) => Some(cmp::min(max_timeout, Duration::new(n as u64, 0))), }, _ => None, } } fn build_lock_prop(lock: &DavLock, full: bool) -> Element { let mut actlock = Element::new2("D:activelock"); let mut elem = Element::new2("D:lockscope"); elem.push_element(match lock.shared { false => Element::new2("D:exclusive"), true => Element::new2("D:shared"), }); actlock.push_element(elem); let mut elem = Element::new2("D:locktype"); elem.push_element(Element::new2("D:write")); actlock.push_element(elem); actlock.push_element( Element::new2("D:depth").text( match lock.deep { false => "0", true => "Infinity", } .to_string(), ), ); actlock.push_element(Element::new2("D:timeout").text(match lock.timeout { None => "Infinite".to_string(), Some(d) => format!("Second-{}", d.as_secs()), })); let mut locktokenelem = Element::new2("D:locktoken"); locktokenelem.push_element(Element::new2("D:href").text(lock.token.clone())); actlock.push_element(locktokenelem); let mut lockroot = Element::new2("D:lockroot"); lockroot.push_element(Element::new2("D:href").text(lock.path.with_prefix().as_url_string())); actlock.push_element(lockroot); if let Some(ref o) = lock.owner { actlock.push_element(*o.clone()); } if !full { return actlock; } let mut ldis = Element::new2("D:lockdiscovery"); ldis.push_element(actlock); let mut prop = Element::new2("D:prop").ns("D", "DAV:"); prop.push_element(ldis); prop } dav-server-0.11.0/src/handle_mkcol.rs000064400000000000000000000040071046102023000155370ustar 00000000000000use headers::HeaderMapExt; use http::{Request, Response, StatusCode}; use crate::body::Body; use crate::conditional::*; use crate::davheaders; use crate::fs::*; use crate::{DavError, DavInner, DavResult}; impl DavInner { pub(crate) async fn handle_mkcol(&self, req: &Request<()>) -> DavResult> { let mut path = self.path(req); let meta = self.fs.metadata(&path, &self.credentials).await; // check the If and If-* headers. let res = if_match_get_tokens( req, meta.as_ref().map(|v| v.as_ref()).ok(), self.fs.as_ref(), &self.ls, &path, &self.credentials, ) .await; let tokens = match res { Ok(t) => t, Err(s) => return Err(DavError::Status(s)), }; // if locked check if we hold that lock. if let Some(ref locksystem) = self.ls { let principal = self.principal.as_deref(); if let Err(_l) = locksystem .check(&path, principal, false, false, &tokens) .await { return Err(DavError::Status(StatusCode::LOCKED)); } } let mut res = Response::new(Body::empty()); match self.fs.create_dir(&path, &self.credentials).await { // RFC 4918 9.3.1 MKCOL Status Codes. Err(FsError::Exists) => return Err(DavError::Status(StatusCode::METHOD_NOT_ALLOWED)), Err(FsError::NotFound) => return Err(DavError::Status(StatusCode::CONFLICT)), Err(e) => return Err(DavError::FsError(e)), Ok(()) => { if path.is_collection() { path.add_slash(); res.headers_mut().typed_insert(davheaders::ContentLocation( path.with_prefix().as_url_string(), )); } *res.status_mut() = StatusCode::CREATED; } } Ok(res) } } dav-server-0.11.0/src/handle_options.rs000064400000000000000000000077231046102023000161350ustar 00000000000000use headers::HeaderMapExt; use http::{Request, Response}; use crate::body::Body; use crate::util::{DavMethod, dav_method}; use crate::{DavInner, DavResult}; impl DavInner { pub(crate) async fn handle_options(&self, req: &Request<()>) -> DavResult> { let mut res = Response::new(Body::empty()); let h = res.headers_mut(); // We could simply not report webdav level 2 support if self.allow doesn't // contain LOCK/UNLOCK. However we do advertise support, since there might // be LOCK/UNLOCK support in another part of the URL space. #[cfg(all(feature = "caldav", feature = "carddav"))] let dav = "1,2,3,sabredav-partialupdate,calendar-access,addressbook"; #[cfg(all(feature = "caldav", not(feature = "carddav")))] let dav = "1,2,3,sabredav-partialupdate,calendar-access"; #[cfg(all(feature = "carddav", not(feature = "caldav")))] let dav = "1,2,3,sabredav-partialupdate,addressbook"; #[cfg(not(any(feature = "caldav", feature = "carddav")))] let dav = "1,2,3,sabredav-partialupdate"; h.insert("DAV", dav.parse().unwrap()); h.insert("MS-Author-Via", "DAV".parse().unwrap()); h.typed_insert(headers::ContentLength(0)); // Helper to add method to array if method is in fact // allowed. If the current method is not OPTIONS, leave // out the current method since we're probably called // for DavMethodNotAllowed. let method = dav_method(req.method()).unwrap_or(DavMethod::Options); let islock = |m| m == DavMethod::Lock || m == DavMethod::Unlock; let mm = |v: &mut Vec, m: &str, y: DavMethod| { if (y == DavMethod::Options || (y != method || islock(y) != islock(method))) && (!islock(y) || self.ls.is_some()) && self.allow.map(|x| x.contains(y)).unwrap_or(true) { v.push(m.to_string()); } }; let path = self.path(req); let meta = self.fs.metadata(&path, &self.credentials).await; let is_unmapped = meta.is_err(); let is_file = meta.map(|m| m.is_file()).unwrap_or_default(); let is_star = path.is_star() && method == DavMethod::Options; let mut v = Vec::new(); if is_unmapped && !is_star { mm(&mut v, "OPTIONS", DavMethod::Options); mm(&mut v, "MKCOL", DavMethod::MkCol); mm(&mut v, "PUT", DavMethod::Put); mm(&mut v, "LOCK", DavMethod::Lock); #[cfg(feature = "caldav")] mm(&mut v, "MKCALENDAR", DavMethod::MkCalendar); #[cfg(feature = "carddav")] mm(&mut v, "MKADDRESSBOOK", DavMethod::MkAddressbook); } else { if is_file || is_star { mm(&mut v, "HEAD", DavMethod::Head); mm(&mut v, "GET", DavMethod::Get); mm(&mut v, "PATCH", DavMethod::Patch); mm(&mut v, "PUT", DavMethod::Put); } mm(&mut v, "OPTIONS", DavMethod::Options); mm(&mut v, "PROPFIND", DavMethod::PropFind); mm(&mut v, "COPY", DavMethod::Copy); if path.as_url_string() != "/" { mm(&mut v, "MOVE", DavMethod::Move); mm(&mut v, "DELETE", DavMethod::Delete); } mm(&mut v, "LOCK", DavMethod::Lock); mm(&mut v, "UNLOCK", DavMethod::Unlock); #[cfg(any(feature = "caldav", feature = "carddav"))] mm(&mut v, "REPORT", DavMethod::Report); #[cfg(feature = "caldav")] if is_unmapped { mm(&mut v, "MKCALENDAR", DavMethod::MkCalendar); } #[cfg(feature = "carddav")] if is_unmapped { mm(&mut v, "MKADDRESSBOOK", DavMethod::MkAddressbook); } } let a = v.join(",").parse().unwrap(); res.headers_mut().insert("allow", a); Ok(res) } } dav-server-0.11.0/src/handle_props.rs000064400000000000000000001445211046102023000156030ustar 00000000000000use std::borrow::Cow; use std::collections::HashMap; use std::convert::TryFrom; use std::io::{self, Cursor}; use std::sync::LazyLock; use futures_util::{FutureExt, StreamExt, future::BoxFuture}; use headers::HeaderMapExt; use http::{Request, Response, StatusCode}; use crate::xmltree_ext::*; use xml::EmitterConfig; use xml::common::XmlVersion; use xml::writer::EventWriter; use xml::writer::XmlEvent as XmlWEvent; use xmltree::{Element, XMLNode}; use crate::async_stream::AsyncStream; use crate::body::Body; use crate::conditional::if_match_get_tokens; use crate::davheaders; use crate::davpath::*; use crate::errors::*; use crate::fs::*; use crate::handle_lock::{list_lockdiscovery, list_supportedlock}; use crate::ls::*; use crate::util::MemBuffer; use crate::util::{ dav_xml_error, systemtime_to_httpdate, systemtime_to_rfc3339_without_nanosecond, }; use crate::{DavInner, DavResult}; #[cfg(feature = "caldav")] use crate::caldav::*; #[cfg(feature = "carddav")] use crate::carddav::*; const NS_APACHE_URI: &str = "http://apache.org/dav/props/"; const NS_DAV_URI: &str = "DAV:"; const NS_MS_URI: &str = "urn:schemas-microsoft-com:"; const NS_NEXTCLOUD_URI: &str = "http://nextcloud.org/ns"; const NS_OWNCLOUD_URI: &str = "http://owncloud.org/ns"; // list returned by PROPFIND . #[cfg(all(feature = "caldav", feature = "carddav"))] const PROPNAME_STR: &[&str] = &[ "D:creationdate", "D:displayname", "D:getcontentlanguage", "D:getcontentlength", "D:getcontenttype", "D:getetag", "D:getlastmodified", "D:lockdiscovery", "D:resourcetype", "D:supportedlock", "D:quota-available-bytes", "D:quota-used-bytes", "A:executable", "Z:Win32LastAccessTime", "C:calendar-description", "C:calendar-timezone", "C:supported-calendar-component-set", "C:supported-calendar-data", "C:calendar-home-set", "CARD:addressbook-description", "CARD:supported-address-data", "CARD:addressbook-home-set", "CARD:max-resource-size", ]; #[cfg(all(feature = "caldav", not(feature = "carddav")))] const PROPNAME_STR: &[&str] = &[ "D:creationdate", "D:displayname", "D:getcontentlanguage", "D:getcontentlength", "D:getcontenttype", "D:getetag", "D:getlastmodified", "D:lockdiscovery", "D:resourcetype", "D:supportedlock", "D:quota-available-bytes", "D:quota-used-bytes", "A:executable", "Z:Win32LastAccessTime", "C:calendar-description", "C:calendar-timezone", "C:supported-calendar-component-set", "C:supported-calendar-data", "C:calendar-home-set", ]; #[cfg(all(feature = "carddav", not(feature = "caldav")))] const PROPNAME_STR: &[&str] = &[ "D:creationdate", "D:displayname", "D:getcontentlanguage", "D:getcontentlength", "D:getcontenttype", "D:getetag", "D:getlastmodified", "D:lockdiscovery", "D:resourcetype", "D:supportedlock", "D:quota-available-bytes", "D:quota-used-bytes", "A:executable", "Z:Win32LastAccessTime", "CARD:addressbook-description", "CARD:supported-address-data", "CARD:addressbook-home-set", "CARD:max-resource-size", ]; #[cfg(not(any(feature = "caldav", feature = "carddav")))] const PROPNAME_STR: &[&str] = &[ "D:creationdate", "D:displayname", "D:getcontentlanguage", "D:getcontentlength", "D:getcontenttype", "D:getetag", "D:getlastmodified", "D:lockdiscovery", "D:resourcetype", "D:supportedlock", "D:quota-available-bytes", "D:quota-used-bytes", "A:executable", "Z:Win32LastAccessTime", ]; // properties returned by PROPFIND or empty body. #[cfg(all(feature = "caldav", feature = "carddav"))] const ALLPROP_STR: &[&str] = &[ "D:creationdate", "D:displayname", "D:getcontentlanguage", "D:getcontentlength", "D:getcontenttype", "D:getetag", "D:getlastmodified", "D:lockdiscovery", "D:resourcetype", "D:supportedlock", "C:supported-calendar-component-set", "C:supported-calendar-data", "CARD:supported-address-data", ]; #[cfg(all(feature = "caldav", not(feature = "carddav")))] const ALLPROP_STR: &[&str] = &[ "D:creationdate", "D:displayname", "D:getcontentlanguage", "D:getcontentlength", "D:getcontenttype", "D:getetag", "D:getlastmodified", "D:lockdiscovery", "D:resourcetype", "D:supportedlock", "C:supported-calendar-component-set", "C:supported-calendar-data", ]; #[cfg(all(feature = "carddav", not(feature = "caldav")))] const ALLPROP_STR: &[&str] = &[ "D:creationdate", "D:displayname", "D:getcontentlanguage", "D:getcontentlength", "D:getcontenttype", "D:getetag", "D:getlastmodified", "D:lockdiscovery", "D:resourcetype", "D:supportedlock", "CARD:supported-address-data", ]; #[cfg(not(any(feature = "caldav", feature = "carddav")))] const ALLPROP_STR: &[&str] = &[ "D:creationdate", "D:displayname", "D:getcontentlanguage", "D:getcontentlength", "D:getcontenttype", "D:getetag", "D:getlastmodified", "D:lockdiscovery", "D:resourcetype", "D:supportedlock", ]; // properties returned by PROPFIND with empty body for Microsoft clients. const MS_ALLPROP_STR: &[&str] = &[ "D:creationdate", "D:displayname", "D:getcontentlanguage", "D:getcontentlength", "D:getcontenttype", "D:getetag", "D:getlastmodified", "D:lockdiscovery", "D:resourcetype", "D:supportedlock", "Z:Win32CreationTime", "Z:Win32FileAttributes", "Z:Win32LastAccessTime", "Z:Win32LastModifiedTime", ]; static ALLPROP: LazyLock> = LazyLock::new(|| init_staticprop(ALLPROP_STR)); static MS_ALLPROP: LazyLock> = LazyLock::new(|| init_staticprop(MS_ALLPROP_STR)); static PROPNAME: LazyLock> = LazyLock::new(|| init_staticprop(PROPNAME_STR)); type Emitter = EventWriter; type Sender = crate::async_stream::Sender; struct StatusElement { status: StatusCode, element: Element, } pub(crate) struct PropWriter { emitter: Emitter, tx: Option, name: String, props: Vec, fs: Box>, ls: Option>, useragent: String, q_cache: QuotaCache, credentials: C, principal: Option, } #[derive(Default, Clone, Copy)] struct QuotaCache { q_state: u32, q_used: u64, q_total: Option, } fn init_staticprop(p: &[&str]) -> Vec { let mut v = Vec::new(); for a in p { let mut e = Element::new2(*a); e.namespace = match e.prefix.as_deref() { Some("D") => Some(NS_DAV_URI.to_string()), Some("A") => Some(NS_APACHE_URI.to_string()), Some("Z") => Some(NS_MS_URI.to_string()), _ => None, }; v.push(e); } v } impl DavInner { pub(crate) async fn handle_propfind( self, req: &Request<()>, xmldata: &[u8], ) -> DavResult> { // No checks on If: and If-* headers here, because I do not see // the point and there's nothing in RFC4918 that indicates we should. let mut res = Response::new(Body::empty()); res.headers_mut() .typed_insert(headers::CacheControl::new().with_no_cache()); res.headers_mut().typed_insert(headers::Pragma::no_cache()); let depth = match req.headers().typed_get::() { Some(davheaders::Depth::Infinity) => { if req.headers().typed_get::().is_none() { let ct = "application/xml; charset=utf-8".to_owned(); res.headers_mut().typed_insert(davheaders::ContentType(ct)); *res.status_mut() = StatusCode::NOT_IMPLEMENTED; *res.body_mut() = dav_xml_error(""); return Ok(res); } davheaders::Depth::Infinity } Some(d) => d, None => davheaders::Depth::Default, }; // path and meta let mut path = self.path(req); let meta = self.fs.metadata(&path, &self.credentials).await?; let meta = self.fixpath(&mut res, &mut path, meta); let mut root = None; if !xmldata.is_empty() { root = match Element::parse(Cursor::new(xmldata)) { Ok(t) => { if t.name == "propfind" && t.namespace.as_deref() == Some("DAV:") { Some(t) } else { return Err(DavError::XmlParseError); } } Err(_) => return Err(DavError::XmlParseError), }; } let (name, props) = match root { None => ("allprop", Vec::new()), Some(mut elem) => { let includes = elem .take_child("includes") .map_or(Vec::new(), |n| n.take_child_elems()); match elem .child_elems_into_iter() .find(|e| e.name == "propname" || e.name == "prop" || e.name == "allprop") { Some(elem) => match elem.name.as_str() { "propname" => ("propname", Vec::new()), "prop" => ("prop", elem.take_child_elems()), "allprop" => ("allprop", includes), _ => return Err(DavError::XmlParseError), }, None => return Err(DavError::XmlParseError), } } }; trace!("propfind: type request: {name}"); let mut pw = PropWriter::new( req, &mut res, name, props, self.fs.clone(), self.ls.as_ref(), self.principal.clone(), self.credentials.clone(), #[cfg(any(feature = "caldav", feature = "carddav"))] &path, )?; *res.body_mut() = Body::from(AsyncStream::new(|tx| async move { pw.set_tx(tx); let is_dir = meta.is_dir(); // Handle Depth::Default case: no target resource, only Depth 1 children if depth != davheaders::Depth::Default { pw.write_props(&path, meta).await?; pw.flush().await?; } if is_dir && (depth == davheaders::Depth::One || depth == davheaders::Depth::Default || depth == davheaders::Depth::Infinity) { self.propfind_directory(&path, depth, &mut pw).await?; } pw.close().await?; Ok(()) })); Ok(res) } fn propfind_directory<'a>( &'a self, path: &'a DavPath, depth: davheaders::Depth, propwriter: &'a mut PropWriter, ) -> BoxFuture<'a, DavResult<()>> { async move { let readdir_meta = match self.hide_symlinks { Some(true) | None => ReadDirMeta::DataSymlink, Some(false) => ReadDirMeta::Data, }; let mut entries = match self .fs .read_dir(path, readdir_meta, &self.credentials) .await { Ok(entries) => entries, Err(e) => { // if we cannot read_dir, just skip it. error!("read_dir error {e:?}"); return Ok(()); } }; while let Some(dirent) = entries.next().await { let dirent = match dirent { Ok(dirent) => dirent, Err(e) => { trace!("next dir entry error happened. Skipping {e:?}"); continue; } }; let mut npath = path.clone(); npath.push_segment(&dirent.name()); let meta = match dirent.metadata().await { Ok(meta) => meta, Err(e) => { trace!("metadata error on {npath}. Skipping {e:?}"); continue; } }; if meta.is_symlink() { continue; } let is_dir = meta.is_dir(); if is_dir { npath.add_slash(); } propwriter.write_props(&npath, meta).await?; propwriter.flush().await?; // For Depth::Default, treat it like Depth::One (no recursion) // Only recurse for Depth::Infinity if depth == davheaders::Depth::Infinity && is_dir { self.propfind_directory(&npath, depth, propwriter).await?; } } Ok(()) } .boxed() } // set/change a live property. returns StatusCode::CONTINUE if // this wasnt't a live property (or, if we want it handled // as a dead property, e.g. DAV:displayname). fn liveprop_set(&self, prop: &Element, can_deadprop: bool) -> StatusCode { match prop.namespace.as_deref() { Some(NS_DAV_URI) => { match prop.name.as_str() { "getcontentlanguage" => { if prop.get_text().is_none() || prop.has_child_elems() { return StatusCode::CONFLICT; } // FIXME only here to make "litmus" happy, really... if let Some(s) = prop.get_text() && davheaders::ContentLanguage::try_from(s.as_ref()).is_err() { return StatusCode::CONFLICT; } if can_deadprop { StatusCode::CONTINUE } else { StatusCode::FORBIDDEN } } "displayname" => { if prop.get_text().is_none() || prop.has_child_elems() { return StatusCode::CONFLICT; } if can_deadprop { StatusCode::CONTINUE } else { StatusCode::FORBIDDEN } } "getlastmodified" => { // we might allow setting modified time // by using utimes() on unix. Not yet though. if prop.get_text().is_none() || prop.has_child_elems() { return StatusCode::CONFLICT; } StatusCode::FORBIDDEN } _ => StatusCode::FORBIDDEN, } } Some(NS_APACHE_URI) => { match prop.name.as_str() { "executable" => { // we could allow toggling the execute bit. // to be implemented. if prop.get_text().is_none() || prop.has_child_elems() { return StatusCode::CONFLICT; } StatusCode::FORBIDDEN } _ => StatusCode::FORBIDDEN, } } Some(NS_MS_URI) => { match prop.name.as_str() { "Win32CreationTime" | "Win32FileAttributes" | "Win32LastAccessTime" | "Win32LastModifiedTime" => { if prop.get_text().is_none() || prop.has_child_elems() { return StatusCode::CONFLICT; } // Always report back that we successfully // changed these, even if we didn't -- // makes the windows webdav client work. StatusCode::OK } _ => StatusCode::FORBIDDEN, } } _ => StatusCode::CONTINUE, } } // In general, live properties cannot be removed, with the // exception of getcontentlanguage and displayname. fn liveprop_remove(&self, prop: &Element, can_deadprop: bool) -> StatusCode { match prop.namespace.as_deref() { Some(NS_DAV_URI) => match prop.name.as_str() { "getcontentlanguage" | "displayname" => { if can_deadprop { StatusCode::OK } else { StatusCode::FORBIDDEN } } _ => StatusCode::FORBIDDEN, }, Some(NS_APACHE_URI) | Some(NS_MS_URI) => StatusCode::FORBIDDEN, _ => StatusCode::CONTINUE, } } pub(crate) async fn handle_proppatch( self, req: &Request<()>, xmldata: &[u8], ) -> DavResult> { let mut res = Response::new(Body::empty()); // file must exist. let mut path = self.path(req); let meta = self.fs.metadata(&path, &self.credentials).await?; let meta = self.fixpath(&mut res, &mut path, meta); // check the If and If-* headers. let tokens = match if_match_get_tokens( req, Some(meta.as_ref()), self.fs.as_ref(), &self.ls, &path, &self.credentials, ) .await { Ok(t) => t, Err(s) => return Err(s.into()), }; // if locked check if we hold that lock. if let Some(ref locksystem) = self.ls { let principal = self.principal.as_deref(); if let Err(_l) = locksystem .check(&path, principal, false, false, &tokens) .await { return Err(StatusCode::LOCKED.into()); } } trace!(target: "xml", "proppatch input:\n{}]\n", std::string::String::from_utf8_lossy(xmldata)); // parse xml let tree = Element::parse2(Cursor::new(xmldata))?; if tree.name != "propertyupdate" { return Err(DavError::XmlParseError); } let mut patch = Vec::new(); let mut ret = Vec::new(); let can_deadprop = self.fs.have_props(&path, &self.credentials).await; // walk over the element tree and feed "set" and "remove" items to // the liveprop_set/liveprop_remove functions. If skipped by those, // gather .them in the "patch" Vec to be processed as dead properties. for elem in tree.child_elems_iter() { for n in elem .child_elems_iter() .filter(|e| e.name == "prop") .flat_map(|e| e.child_elems_iter()) { match elem.name.as_str() { "set" => match self.liveprop_set(n, can_deadprop) { StatusCode::CONTINUE => patch.push((true, element_to_davprop_full(n))), s => ret.push((s, element_to_davprop(n))), }, "remove" => match self.liveprop_remove(n, can_deadprop) { StatusCode::CONTINUE => patch.push((false, element_to_davprop(n))), s => ret.push((s, element_to_davprop(n))), }, _ => {} } } } // if any set/remove failed, stop processing here. if ret.iter().any(|(s, _)| s != &StatusCode::OK) { ret = ret .into_iter() .map(|(s, p)| { if s == StatusCode::OK { (StatusCode::FAILED_DEPENDENCY, p) } else { (s, p) } }) .collect::>(); ret.extend( patch .into_iter() .map(|(_, p)| (StatusCode::FAILED_DEPENDENCY, p)), ); } else if !patch.is_empty() { // hmmm ... we assume nothing goes wrong here at the // moment. if it does, we should roll back the earlier // made changes to live props, but come on, we're not // builing a transaction engine here. let deadret = self.fs.patch_props(&path, patch, &self.credentials).await?; ret.extend(deadret.into_iter()); } // group by statuscode. let mut hm = HashMap::new(); for (code, prop) in ret.into_iter() { hm.entry(code).or_insert_with(Vec::new); let v = hm.get_mut(&code).unwrap(); v.push(davprop_to_element(prop)); } // And reply. let mut pw = PropWriter::new( req, &mut res, "propertyupdate", Vec::new(), self.fs.clone(), None, self.principal.clone(), self.credentials, #[cfg(any(feature = "caldav", feature = "carddav"))] &path, )?; *res.body_mut() = Body::from(AsyncStream::new(|tx| async move { pw.set_tx(tx); pw.write_propresponse(&path, hm)?; pw.close().await?; Ok::<_, io::Error>(()) })); Ok(res) } } impl PropWriter { #[allow(clippy::borrowed_box)] #[allow(clippy::too_many_arguments)] pub fn new( req: &Request<()>, res: &mut Response, name: &str, mut props: Vec, fs: Box>, ls: Option<&Box>, principal: Option, credentials: C, #[cfg(any(feature = "caldav", feature = "carddav"))] dav_path: &DavPath, ) -> DavResult { let contenttype = "application/xml; charset=utf-8".parse().unwrap(); res.headers_mut().insert("content-type", contenttype); *res.status_mut() = StatusCode::MULTI_STATUS; let mut emitter = EventWriter::new_with_config( MemBuffer::new(), EmitterConfig { normalize_empty_elements: false, perform_indent: false, indent_string: Cow::Borrowed(""), ..Default::default() }, ); emitter.write(XmlWEvent::StartDocument { version: XmlVersion::Version10, encoding: Some("utf-8"), standalone: None, })?; // user-agent header. let ua = match req.headers().get("user-agent") { Some(s) => s.to_str().unwrap_or(""), None => "", }; if name != "prop" && name != "propertyupdate" { let mut v = Vec::new(); let iter = if name == "allprop" { if ua.contains("Microsoft") { MS_ALLPROP.iter() } else { ALLPROP.iter() } } else { PROPNAME.iter() }; for a in iter { if !props .iter() .any(|e| a.namespace == e.namespace && a.name == e.name) { v.push(a.clone()); } } props.append(&mut v); } // check the prop namespaces to see what namespaces // we need to put in the preamble. let mut ev = XmlWEvent::start_element("D:multistatus").ns("D", NS_DAV_URI); if name != "propertyupdate" { let mut a = false; let mut m = false; let mut nc = false; // Nextcloud let mut oc = false; // OwnCloud #[cfg(feature = "caldav")] let mut c = false; // CalDAV #[cfg(feature = "carddav")] let mut card = false; // CardDAV for prop in &props { match prop.namespace.as_deref() { Some(NS_APACHE_URI) => a = true, Some(NS_MS_URI) => m = true, Some(NS_NEXTCLOUD_URI) => nc = true, Some(NS_OWNCLOUD_URI) => oc = true, #[cfg(feature = "caldav")] Some(NS_CALDAV_URI) => c = true, #[cfg(feature = "carddav")] Some(NS_CARDDAV_URI) => card = true, _ => {} } } if a { ev = ev.ns("A", NS_APACHE_URI); } if m { ev = ev.ns("Z", NS_MS_URI); } if nc { ev = ev.ns("nc", NS_NEXTCLOUD_URI); } if oc { ev = ev.ns("oc", NS_OWNCLOUD_URI); } #[cfg(feature = "caldav")] if c || req.uri().path().starts_with(&format!( "{}{}", dav_path.prefix(), DEFAULT_CALDAV_DIRECTORY )) { ev = ev.ns("C", NS_CALDAV_URI); } #[cfg(feature = "carddav")] if card || req.uri().path().starts_with(&format!( "{}{}", dav_path.prefix(), DEFAULT_CARDDAV_DIRECTORY )) { ev = ev.ns("CARD", NS_CARDDAV_URI); } } emitter.write(ev)?; Ok(Self { emitter, tx: None, name: name.to_string(), props, fs, ls: ls.cloned(), useragent: ua.to_string(), q_cache: Default::default(), credentials, principal, }) } pub fn set_tx(&mut self, tx: Sender) { self.tx = Some(tx) } fn build_elem( &self, content: bool, pfx: &str, e: &Element, text: T, ) -> DavResult where T: Into, { let mut elem = Element { prefix: Some(pfx.to_string()), namespace: None, namespaces: None, name: e.name.clone(), attributes: HashMap::new(), children: Vec::new(), }; if content { let t: String = text.into(); if !t.is_empty() { elem.children.push(XMLNode::Text(t)); } } Ok(StatusElement { status: StatusCode::OK, element: elem, }) } async fn get_quota<'a>( &'a self, qc: &'a mut QuotaCache, path: &'a DavPath, meta: &'a dyn DavMetaData, ) -> FsResult<(u64, Option)> { // do lookup only once. match qc.q_state { 0 => match self.fs.get_quota(&self.credentials).await { Err(e) => { qc.q_state = 1; return Err(e); } Ok((u, t)) => { qc.q_used = u; qc.q_total = t; qc.q_state = 2; } }, 1 => return Err(FsError::NotImplemented), _ => {} } // if not "/", return for "used" just the size of this file/dir. let used = if path.as_bytes() == b"/" { qc.q_used } else { meta.len() }; // calculate available space. let avail = qc.q_total.map(|total| total.saturating_sub(used)); Ok((used, avail)) } async fn build_prop<'a>( &'a self, prop: &'a Element, path: &'a DavPath, meta: &'a dyn DavMetaData, qc: &'a mut QuotaCache, docontent: bool, ) -> DavResult { // in some cases, a live property might be stored in the // dead prop database, like DAV:displayname. let mut try_deadprop = false; let mut pfx = ""; match prop.namespace.as_deref() { Some(NS_DAV_URI) => { pfx = "D"; match prop.name.as_str() { "creationdate" => { if let Ok(time) = meta.created() { let tm = systemtime_to_rfc3339_without_nanosecond(time); return self.build_elem(docontent, pfx, prop, tm); } // use ctime instead - apache seems to do this. if let Ok(ctime) = meta.status_changed() { let mut time = ctime; if let Ok(mtime) = meta.modified() && mtime < ctime { time = mtime; } let tm = systemtime_to_rfc3339_without_nanosecond(time); return self.build_elem(docontent, pfx, prop, tm); } } "displayname" | "getcontentlanguage" => { try_deadprop = true; } "getetag" => { if let Some(etag) = meta.etag() { return self.build_elem(docontent, pfx, prop, etag); } } "getcontentlength" => { if !meta.is_dir() { return self.build_elem(docontent, pfx, prop, meta.len().to_string()); } } "getcontenttype" => { return if meta.is_dir() { self.build_elem(docontent, pfx, prop, "httpd/unix-directory") } else { self.build_elem(docontent, pfx, prop, path.get_mime_type_str()) }; } "getlastmodified" => { if let Ok(time) = meta.modified() { let tm = systemtime_to_httpdate(time); return self.build_elem(docontent, pfx, prop, tm); } } "resourcetype" => { let mut elem = prop.clone(); if meta.is_dir() && docontent { let dir = Element::new2("D:collection"); elem.children.push(XMLNode::Element(dir)); #[cfg(feature = "caldav")] if meta.is_calendar(path) { let calendar = Element::new2("C:calendar"); elem.children.push(XMLNode::Element(calendar)); } #[cfg(feature = "carddav")] if meta.is_addressbook(path) { let addressbook = Element::new2("CARD:addressbook"); elem.children.push(XMLNode::Element(addressbook)); } } return Ok(StatusElement { status: StatusCode::OK, element: elem, }); } "current-user-principal" => { if let Some(pr) = &self.principal { let mut elem = prop.clone(); let mut principal_href = Element::new2("D:href"); principal_href = principal_href.text(pr.clone()); elem.children .push(xmltree::XMLNode::Element(principal_href)); return Ok(StatusElement { status: StatusCode::OK, element: elem, }); } } "supportedlock" => { return Ok(StatusElement { status: StatusCode::OK, element: list_supportedlock(self.ls.as_ref()), }); } "lockdiscovery" => { return Ok(StatusElement { status: StatusCode::OK, element: list_lockdiscovery(self.ls.as_ref(), path).await, }); } "quota-available-bytes" => { if let Ok((_, Some(avail))) = self.get_quota(qc, path, meta).await { return self.build_elem(docontent, pfx, prop, avail.to_string()); } } "quota-used-bytes" => { if let Ok((used, _)) = self.get_quota(qc, path, meta).await { let used = if self.useragent.contains("WebDAVFS") { // Need this on MacOs, otherwise the value is off // by a factor of 10 or so .. ?!?!!? format!("{used:014}") } else { used.to_string() }; return self.build_elem(docontent, pfx, prop, used); } } _ => {} } } Some(NS_APACHE_URI) => { pfx = "A"; if prop.name.as_str() == "executable" && let Ok(x) = meta.executable() { let b = if x { "T" } else { "F" }; return self.build_elem(docontent, pfx, prop, b); } } #[cfg(feature = "caldav")] Some(NS_CALDAV_URI) => { pfx = "C"; if meta.is_calendar(path) { match prop.name.as_str() { "supported-calendar-component-set" => { let components = vec![ CalendarComponentType::VEvent, CalendarComponentType::VTodo, CalendarComponentType::VJournal, CalendarComponentType::VFreeBusy, ]; let elem = create_supported_calendar_component_set(&components); return Ok(StatusElement { status: StatusCode::OK, element: elem, }); } "supported-calendar-data" => { let elem = create_supported_calendar_data(); return Ok(StatusElement { status: StatusCode::OK, element: elem, }); } "calendar-description" => { if let Ok(props) = self.fs.get_props(path, docontent, &self.credentials).await { for prop_item in props { if prop_item.name.contains("calendar-description") { return Ok(StatusElement { status: StatusCode::OK, element: davprop_to_element(prop_item), }); } } } } "calendar-timezone" => { // Default to UTC if not set let timezone = "BEGIN:VTIMEZONE\r\nTZID:UTC\r\nEND:VTIMEZONE\r\n"; return self.build_elem(docontent, pfx, prop, timezone); } "max-resource-size" => { return self.build_elem(docontent, pfx, prop, "1048576"); // 1MB } "min-date-time" => { return self.build_elem(docontent, pfx, prop, "19000101T000000Z"); } "max-date-time" => { return self.build_elem(docontent, pfx, prop, "20991231T235959Z"); } _ => {} } } } #[cfg(feature = "carddav")] Some(NS_CARDDAV_URI) => { pfx = "CARD"; if meta.is_addressbook(path) { match prop.name.as_str() { "supported-address-data" => { let elem = create_supported_address_data(); return Ok(StatusElement { status: StatusCode::OK, element: elem, }); } "addressbook-description" => { if let Ok(props) = self.fs.get_props(path, docontent, &self.credentials).await { for prop_item in props { if prop_item.name.contains("addressbook-description") { return Ok(StatusElement { status: StatusCode::OK, element: davprop_to_element(prop_item), }); } } } } "max-resource-size" => { let size = DEFAULT_MAX_RESOURCE_SIZE.to_string(); return self.build_elem(docontent, pfx, prop, size); } _ => {} } } } Some(NS_MS_URI) => { pfx = "Z"; match prop.name.as_str() { "Win32CreationTime" => { if let Ok(time) = meta.created() { let tm = systemtime_to_httpdate(time); return self.build_elem(docontent, pfx, prop, tm); } // use ctime instead - apache seems to do this. if let Ok(ctime) = meta.status_changed() { let mut time = ctime; if let Ok(mtime) = meta.modified() && mtime < ctime { time = mtime; } let tm = systemtime_to_httpdate(time); return self.build_elem(docontent, pfx, prop, tm); } } "Win32LastAccessTime" => { if let Ok(time) = meta.accessed() { let tm = systemtime_to_httpdate(time); return self.build_elem(docontent, pfx, prop, tm); } } "Win32LastModifiedTime" => { if let Ok(time) = meta.modified() { let tm = systemtime_to_httpdate(time); return self.build_elem(docontent, pfx, prop, tm); } } "Win32FileAttributes" => { let mut attr = 0u32; // Enable when we implement permissions() on DavMetaData. //if meta.permissions().readonly() { // attr |= 0x0001; //} if path.file_name_bytes().starts_with(b".") { attr |= 0x0002; } if meta.is_dir() { attr |= 0x0010; } else { // this is the 'Archive' bit, which is set by // default on _all_ files on creation and on // modification. attr |= 0x0020; } return self.build_elem(docontent, pfx, prop, format!("{attr:08x}")); } _ => {} } } _ => { try_deadprop = true; } } if try_deadprop && self.name == "prop" && self.fs.have_props(path, &self.credentials).await { // asking for a specific property. let dprop = element_to_davprop(prop); if let Ok(xml) = self.fs.get_prop(path, dprop, &self.credentials).await && let Ok(e) = Element::parse(Cursor::new(xml)) { return Ok(StatusElement { status: StatusCode::OK, element: e, }); } } let prop = if !pfx.is_empty() { self.build_elem(false, pfx, prop, "") .map(|s| s.element) .unwrap() } else { prop.clone() }; Ok(StatusElement { status: StatusCode::NOT_FOUND, element: prop, }) } pub async fn write_props<'a>( &'a mut self, path: &'a DavPath, meta: Box, ) -> Result<(), DavError> { // A HashMap> for the result. let mut props = HashMap::new(); // Get properties one-by-one let do_content = self.name != "propname"; let mut qc = self.q_cache; for p in &self.props { let res = self .build_prop(p, path, &*meta, &mut qc, do_content) .await?; if res.status == StatusCode::OK { add_sc_elem(&mut props, res.status, res.element); } } self.q_cache = qc; #[cfg(feature = "caldav")] { let path_string = path.to_string(); if path_string == DEFAULT_CALDAV_DIRECTORY || path_string == DEFAULT_CALDAV_DIRECTORY_ENDSLASH { let elem = create_calendar_home_set(path.prefix(), DEFAULT_CALDAV_DIRECTORY_ENDSLASH); add_sc_elem(&mut props, StatusCode::OK, elem); } } #[cfg(feature = "carddav")] { let path_string = path.to_string(); if path_string == DEFAULT_CARDDAV_DIRECTORY || path_string == DEFAULT_CARDDAV_DIRECTORY_ENDSLASH { let elem = create_addressbook_home_set(path.prefix(), DEFAULT_CARDDAV_DIRECTORY_ENDSLASH); add_sc_elem(&mut props, StatusCode::OK, elem); } } // and list props of the filesystem driver if it supports DAV properties if self.fs.have_props(path, &self.credentials).await && let Ok(v) = self.fs.get_props(path, true, &self.credentials).await { v.into_iter() .map(davprop_to_element) .for_each(|e| add_sc_elem(&mut props, StatusCode::OK, e)); } self.write_propresponse(path, props) } pub fn write_propresponse( &mut self, path: &DavPath, props: HashMap>, ) -> Result<(), DavError> { self.emitter.write(XmlWEvent::start_element("D:response"))?; let p = path.with_prefix().as_url_string(); Element::new2("D:href") .text(p) .write_ev(&mut self.emitter)?; let mut keys = props.keys().collect::>(); keys.sort(); for status in keys { let v = props.get(status).unwrap(); self.emitter.write(XmlWEvent::start_element("D:propstat"))?; self.emitter.write(XmlWEvent::start_element("D:prop"))?; for i in v.iter() { i.write_ev(&mut self.emitter)?; } self.emitter.write(XmlWEvent::end_element())?; Element::new2("D:status") .text("HTTP/1.1 ".to_string() + &status.to_string()) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::end_element())?; } self.emitter.write(XmlWEvent::end_element())?; // response Ok(()) } pub async fn flush(&mut self) -> DavResult<()> { let buffer = self.emitter.inner_mut().take(); self.tx.as_mut().unwrap().send(buffer).await; Ok(()) } pub async fn close(&mut self) -> DavResult<()> { let _ = self.emitter.write(XmlWEvent::end_element()); self.flush().await } #[cfg(feature = "caldav")] pub(crate) fn write_calendar_data_response( &mut self, href: &DavPath, etag: &str, calendar_data: &str, ) -> DavResult<()> { self.emitter.write(XmlWEvent::start_element("D:response"))?; let p = href.as_url_string(); Element::new2("D:href") .text(p) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::start_element("D:propstat"))?; self.emitter.write(XmlWEvent::start_element("D:prop"))?; // Write calendar-data element with content let mut elem = Element::new2("C:calendar-data").ns("C", NS_CALDAV_URI); elem.children.push(XMLNode::Text(calendar_data.to_string())); elem.write_ev(&mut self.emitter)?; // Write getetag element Element::new2("D:getetag") .text(etag) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::end_element())?; // D:prop Element::new2("D:status") .text("HTTP/1.1 200 OK".to_string()) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::end_element())?; // D:propstat self.emitter.write(XmlWEvent::end_element())?; // D:response Ok(()) } #[cfg(feature = "caldav")] pub(crate) fn write_calendar_not_found_response(&mut self, href: &str) -> DavResult<()> { self.emitter.write(XmlWEvent::start_element("D:response"))?; Element::new2("D:href") .text(href.to_string()) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::start_element("D:propstat"))?; Element::new2("D:status") .text("HTTP/1.1 404 Not Found".to_string()) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::end_element())?; // D:propstat self.emitter.write(XmlWEvent::end_element())?; // D:response Ok(()) } #[cfg(feature = "carddav")] pub(crate) fn write_vcard_data_response( &mut self, href: &DavPath, etag: &str, vcard_data: &str, ) -> DavResult<()> { self.emitter.write(XmlWEvent::start_element("D:response"))?; let p = href.as_url_string(); Element::new2("D:href") .text(p) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::start_element("D:propstat"))?; self.emitter.write(XmlWEvent::start_element("D:prop"))?; // Write address-data element with content let mut elem = Element::new2("CARD:address-data").ns("CARD", NS_CARDDAV_URI); elem.children.push(XMLNode::Text(vcard_data.to_string())); elem.write_ev(&mut self.emitter)?; // Write getetag element Element::new2("D:getetag") .text(etag) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::end_element())?; // D:prop Element::new2("D:status") .text("HTTP/1.1 200 OK".to_string()) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::end_element())?; // D:propstat self.emitter.write(XmlWEvent::end_element())?; // D:response Ok(()) } #[cfg(feature = "carddav")] pub(crate) fn write_vcard_not_found_response(&mut self, href: &str) -> DavResult<()> { self.emitter.write(XmlWEvent::start_element("D:response"))?; Element::new2("D:href") .text(href.to_string()) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::start_element("D:propstat"))?; Element::new2("D:status") .text("HTTP/1.1 404 Not Found".to_string()) .write_ev(&mut self.emitter)?; self.emitter.write(XmlWEvent::end_element())?; // D:propstat self.emitter.write(XmlWEvent::end_element())?; // D:response Ok(()) } } fn add_sc_elem(hm: &mut HashMap>, sc: StatusCode, e: Element) { hm.entry(sc).or_default(); hm.get_mut(&sc).unwrap().push(e) } fn element_to_davprop_full(elem: &Element) -> DavProp { let mut emitter = EventWriter::new(Cursor::new(Vec::new())); elem.write_ev(&mut emitter).ok(); let xml = emitter.into_inner().into_inner(); DavProp { name: elem.name.clone(), prefix: elem.prefix.clone(), namespace: elem.namespace.clone(), xml: Some(xml), } } fn element_to_davprop(elem: &Element) -> DavProp { DavProp { name: elem.name.clone(), prefix: elem.prefix.clone(), namespace: elem.namespace.clone(), xml: None, } } fn davprop_to_element(prop: DavProp) -> Element { if let Some(xml) = prop.xml { match Element::parse2(Cursor::new(xml)) { Ok(result) => { return result; } Err(error) => { log::error!("davprop_to_element(): {}. Please check your GuardedFileSystem.get_props() implementation. 'xml'should include complete xml tag. Use DavProp::new() to easy create a DavProp with valid xml syntax.", error); } } } let mut elem = Element::new(&prop.name); if let Some(ref ns) = prop.namespace { let pfx = prop.prefix.as_deref().unwrap_or(""); elem = elem.ns(pfx, ns.as_str()); } elem.prefix = prop.prefix; elem.namespace = prop.namespace; elem } dav-server-0.11.0/src/handle_put.rs000064400000000000000000000236711046102023000152520ustar 00000000000000use std::any::Any; use std::error::Error as StdError; use std::io; use std::pin::pin; use bytes::{Buf, Bytes}; use headers::HeaderMapExt; use http::StatusCode as SC; use http::{self, Request, Response}; use http_body::Body as HttpBody; use http_body_util::BodyExt; use crate::body::Body; use crate::conditional::if_match_get_tokens; use crate::davheaders; use crate::fs::*; use crate::{DavError, DavInner, DavResult}; const SABRE: &str = "application/x-sabredav-partialupdate"; // This is a nice hack. If the type 'E' is actually an io::Error or a Box, // convert it back into a real io::Error. If it is a DavError or a Box, // use its Into impl. Otherwise just wrap the error in io::Error::new. // // If we had specialization this would look a lot prettier. // // Also, this is senseless. It's not as if we _do_ anything with the // io::Error, other than noticing "oops an error occured". fn to_ioerror(err: E) -> io::Error where E: StdError + Sync + Send + 'static, { let e = &err as &dyn Any; if e.is::() || e.is::>() { let err = Box::new(err) as Box; match err.downcast::() { Ok(e) => *e, Err(e) => match e.downcast::>() { Ok(e) => *(*e), Err(_) => io::ErrorKind::Other.into(), }, } } else if e.is::() || e.is::>() { let err = Box::new(err) as Box; match err.downcast::() { Ok(e) => (*e).into(), Err(e) => match e.downcast::>() { Ok(e) => (*(*e)).into(), Err(_) => io::ErrorKind::Other.into(), }, } } else { io::Error::other(err) } } impl DavInner { pub(crate) async fn handle_put( self, req: &Request<()>, body: ReqBody, ) -> DavResult> where ReqBody: HttpBody, ReqData: Buf + Send + 'static, ReqError: StdError + Send + Sync + 'static, { let mut start = 0; let mut count = 0; let mut have_count = false; let mut do_range = false; let mut oo = OpenOptions::write(); oo.create = true; oo.truncate = true; if let Some(n) = req.headers().typed_get::() { count = n.0; have_count = true; oo.size = Some(count); } else if let Some(n) = req .headers() .get("X-Expected-Entity-Length") .and_then(|v| v.to_str().ok()) { // macOS Finder, see https://evertpot.com/260/ if let Ok(len) = n.parse() { count = len; have_count = true; oo.size = Some(count); } } let checksum = req .headers() .get("OC-Checksum") .and_then(|v| v.to_str().ok().map(|s| s.to_string())); oo.checksum = checksum; let path = self.path(req); let meta = self.fs.metadata(&path, &self.credentials).await; // close connection on error. let mut res = Response::new(Body::empty()); res.headers_mut().typed_insert(headers::Connection::close()); // SabreDAV style PATCH? if req.method() == http::Method::PATCH { if req .headers() .typed_get::() .is_none_or(|ct| ct.0 != SABRE) { return Err(DavError::StatusClose(SC::UNSUPPORTED_MEDIA_TYPE)); } if !have_count { return Err(DavError::StatusClose(SC::LENGTH_REQUIRED)); }; let r = req .headers() .typed_get::() .ok_or(DavError::StatusClose(SC::BAD_REQUEST))?; match r { davheaders::XUpdateRange::FromTo(b, e) => { if b > e || e - b + 1 != count { return Err(DavError::StatusClose(SC::RANGE_NOT_SATISFIABLE)); } start = b; } davheaders::XUpdateRange::AllFrom(b) => { start = b; } davheaders::XUpdateRange::Last(n) => { if let Ok(ref m) = meta { if n > m.len() { return Err(DavError::StatusClose(SC::RANGE_NOT_SATISFIABLE)); } start = m.len() - n; } } davheaders::XUpdateRange::Append => { oo.append = true; } } do_range = true; oo.truncate = false; } // Apache-style Content-Range header? match req.headers().typed_try_get::() { Ok(Some(range)) => { if let Some((b, e)) = range.bytes_range() { if b > e { return Err(DavError::StatusClose(SC::RANGE_NOT_SATISFIABLE)); } if have_count { if e - b + 1 != count { return Err(DavError::StatusClose(SC::RANGE_NOT_SATISFIABLE)); } } else { count = e - b + 1; have_count = true; } start = b; do_range = true; oo.truncate = false; } } Ok(None) => {} Err(_) => return Err(DavError::StatusClose(SC::BAD_REQUEST)), } // check the If and If-* headers. let tokens = if_match_get_tokens( req, meta.as_ref().map(|v| v.as_ref()).ok(), self.fs.as_ref(), &self.ls, &path, &self.credentials, ); let tokens = match tokens.await { Ok(t) => t, Err(s) => return Err(DavError::StatusClose(s)), }; // if locked check if we hold that lock. if let Some(ref locksystem) = self.ls { let principal = self.principal.as_deref(); if let Err(_l) = locksystem .check(&path, principal, false, false, &tokens) .await { return Err(DavError::StatusClose(SC::LOCKED)); } } // tweak open options. if req .headers() .typed_get::() .is_some_and(|h| h.0 == davheaders::ETagList::Star) { oo.create = false; } if req .headers() .typed_get::() .is_some_and(|h| h.0 == davheaders::ETagList::Star) { oo.create_new = true; } let create = oo.create; let create_new = oo.create_new; let mut file = match self.fs.open(&path, oo, &self.credentials).await { Ok(f) => f, Err(FsError::NotFound) | Err(FsError::Exists) => { let s = if !create || create_new { SC::PRECONDITION_FAILED } else { SC::CONFLICT }; return Err(DavError::StatusClose(s)); } Err(e) => return Err(DavError::FsError(e)), }; if do_range { // seek to beginning of requested data. if file.seek(std::io::SeekFrom::Start(start)).await.is_err() { return Err(DavError::StatusClose(SC::RANGE_NOT_SATISFIABLE)); } } res.headers_mut() .typed_insert(headers::AcceptRanges::bytes()); let mut body = pin!(body); // loop, read body, write to file. let mut total = 0u64; while let Some(data) = body.frame().await { let data_frame = data.map_err(|e| to_ioerror(e))?; let Ok(mut buf) = data_frame.into_data() else { continue; }; total += buf.remaining() as u64; // consistency check. if have_count && total > count { break; } // The `Buf` might actually be a `Bytes`. let b = { let b: &mut dyn std::any::Any = &mut buf; b.downcast_mut::() }; if let Some(bytes) = b { let bytes = std::mem::replace(bytes, Bytes::new()); file.write_bytes(bytes).await?; } else { file.write_buf(Box::new(buf)).await?; } } file.flush().await?; if have_count && total > count { error!("PUT file: sender is sending more bytes than expected"); return Err(DavError::StatusClose(SC::BAD_REQUEST)); } if have_count && total < count { error!("PUT file: premature EOF on input"); return Err(DavError::StatusClose(SC::BAD_REQUEST)); } // Report whether we created or updated the file. *res.status_mut() = match meta { Ok(_) => SC::NO_CONTENT, Err(_) => { res.headers_mut().typed_insert(headers::ContentLength(0)); SC::CREATED } }; // no errors, connection may be kept open. res.headers_mut().remove(http::header::CONNECTION); if let Ok(meta) = file.metadata().await { if let Some(etag) = davheaders::ETag::from_meta(meta.as_ref()) { res.headers_mut().typed_insert(etag); } if let Ok(modified) = meta.modified() { res.headers_mut() .typed_insert(headers::LastModified::from(modified)); } } Ok(res) } } dav-server-0.11.0/src/lib.rs000064400000000000000000000234431046102023000136720ustar 00000000000000//! ## Generic async HTTP/Webdav handler with CalDAV support //! //! [`Webdav`] (RFC4918) is defined as //! HTTP (GET/HEAD/PUT/DELETE) plus a bunch of extension methods (PROPFIND, etc). //! These extension methods are used to manage collections (like unix directories), //! get information on collections (like unix `ls` or `readdir`), rename and //! copy items, lock/unlock items, etc. //! //! [`CalDAV`] (RFC4791) extends WebDAV to provide calendar functionality, //! including calendar collections, calendar resources (iCalendar data), //! and calendar-specific queries. CalDAV support is available with the //! `caldav` feature. //! //! A `handler` is a piece of code that takes a `http::Request`, processes it in some //! way, and then generates a `http::Response`. This library is a `handler` that maps //! the HTTP/Webdav protocol to the filesystem. Or actually, "a" filesystem. Included //! is an adapter for the local filesystem (`localfs`), and an adapter for an //! in-memory filesystem (`memfs`). //! //! So this library can be used as a handler with HTTP servers like [hyper], //! [warp], [actix-web], etc. Either as a correct and complete HTTP handler for //! files (GET/HEAD) or as a handler for the entire Webdav protocol. In the latter case, you can //! mount it as a remote filesystem: Linux, Windows, macOS can all mount Webdav filesystems. //! //! With CalDAV support enabled, it can also serve as a calendar server compatible //! with CalDAV clients like Thunderbird, Apple Calendar, and other calendar applications. //! //! ## Backend interfaces. //! //! The backend interfaces are similar to the ones from the Go `x/net/webdav package`: //! //! - the library contains a [HTTP handler][DavHandler]. //! - you supply a [filesystem][DavFileSystem] for backend storage, which can optionally //! implement reading/writing [DAV properties][DavProp]. If the file system requires //! authorization, implement a [special trait][GuardedFileSystem]. //! - you can supply a [locksystem][DavLockSystem] that handles webdav locks. //! //! The handler in this library works with the standard http types //! from the `http` and `http_body` crates. That means that you can use it //! straight away with http libraries / frameworks that also work with //! those types, like hyper. Compatibility modules for [actix-web][actix-compat] //! and [warp][warp-compat] are also provided. //! //! ## Implemented standards. //! //! Currently [passes the "basic", "copymove", "props", "locks" and "http" //! checks][README_litmus] of the Webdav Litmus Test testsuite. That's all of the base //! [RFC4918] webdav specification. //! //! CalDAV support implements the core CalDAV specification from [RFC4791], including: //! - Calendar collections (MKCALENDAR method) //! - Calendar queries (REPORT method with calendar-query) //! - Calendar multiget (REPORT method with calendar-multiget) //! - CalDAV properties (supported-calendar-component-set, etc.) //! - iCalendar data validation and processing //! //! The litmus test suite also has tests for RFC3744 "acl" and "principal", //! RFC5842 "bind", and RFC3253 "versioning". Those we do not support right now. //! //! The relevant parts of the HTTP RFCs are also implemented, such as the //! preconditions (If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since, //! If-Range), partial transfers (Range). //! //! Also implemented is `partial PUT`, for which there are currently two //! non-standard ways to do it: [`PUT` with the `Content-Range` header][PUT], //! which is what Apache's `mod_dav` implements, and [`PATCH` with the `X-Update-Range` //! header][PATCH] from `SabreDav`. //! //! ## Backends. //! //! Included are two filesystems: //! //! - [`LocalFs`]: serves a directory on the local filesystem //! - [`MemFs`]: ephemeral in-memory filesystem. supports DAV properties. //! //! You're able to implement custom filesystem adapter: //! //! - [`DavFileSystem`]: without authorization. //! - [`GuardedFileSystem`]: when access control is required. //! //! Also included are two locksystems: //! //! - [`MemLs`]: ephemeral in-memory locksystem. //! - [`FakeLs`]: fake locksystem. just enough LOCK/UNLOCK support for macOS/Windows. //! //! External filesystem adapter implementations: //! //! - [`OpendalFs`](https://github.com/apache/opendal/tree/main/integrations/dav-server): //! connects various storage protocols via [OpenDAL](https://github.com/apache/opendal). //! //! ## CalDAV Support //! //! CalDAV functionality is available when the `caldav` feature is enabled: //! //! ```toml //! [dependencies] //! dav-server = { version = "0.9", features = ["caldav"] } //! ``` //! //! This adds support for: //! - `MKCALENDAR` method for creating calendar collections //! - `REPORT` method for calendar queries //! - CalDAV-specific properties and resource types //! - iCalendar data validation //! - Calendar-specific WebDAV extensions //! //! ## Example. //! //! Example server using [hyper] that serves the /tmp directory in r/w mode. You should be //! able to mount this network share from Linux, macOS and Windows. [Examples][examples] //! for other frameworks are also available. //! //! ```no_run //! use std::{convert::Infallible, net::SocketAddr}; //! use hyper::{server::conn::http1, service::service_fn}; //! use hyper_util::rt::TokioIo; //! use tokio::net::TcpListener; //! use dav_server::{fakels::FakeLs, localfs::LocalFs, DavHandler}; //! //! #[tokio::main] //! async fn main() { //! let dir = "/tmp"; //! let addr: SocketAddr = ([127, 0, 0, 1], 4918).into(); //! //! let dav_server = DavHandler::builder() //! .filesystem(LocalFs::new(dir, false, false, false)) //! .locksystem(FakeLs::new()) //! .build_handler(); //! //! let listener = TcpListener::bind(addr).await.unwrap(); //! //! println!("Listening {addr}"); //! //! // We start a loop to continuously accept incoming connections //! loop { //! let (stream, _) = listener.accept().await.unwrap(); //! let dav_server = dav_server.clone(); //! //! // Use an adapter to access something implementing `tokio::io` traits as if they implement //! // `hyper::rt` IO traits. //! let io = TokioIo::new(stream); //! //! // Spawn a tokio task to serve multiple connections concurrently //! tokio::task::spawn(async move { //! // Finally, we bind the incoming connection to our `hello` service //! if let Err(err) = http1::Builder::new() //! // `service_fn` converts our function in a `Service` //! .serve_connection( //! io, //! service_fn({ //! move |req| { //! let dav_server = dav_server.clone(); //! async move { Ok::<_, Infallible>(dav_server.handle(req).await) } //! } //! }), //! ) //! .await //! { //! eprintln!("Failed serving: {err:?}"); //! } //! }); //! } //! } //! ``` //! [DavHandler]: struct.DavHandler.html //! [DavFileSystem]: fs/index.html //! [DavLockSystem]: ls/index.html //! [DavProp]: fs/struct.DavProp.html //! [`WebDav`]: https://tools.ietf.org/html/rfc4918 //! [RFC4918]: https://tools.ietf.org/html/rfc4918 //! [`CalDAV`]: https://tools.ietf.org/html/rfc4791 //! [RFC4791]: https://tools.ietf.org/html/rfc4791 //! [`MemLs`]: memls/index.html //! [`MemFs`]: memfs/index.html //! [`LocalFs`]: localfs/index.html //! [`FakeLs`]: fakels/index.html //! [actix-compat]: actix/index.html //! [warp-compat]: warp/index.html //! [README_litmus]: https://github.com/messense/dav-server-rs/blob/main/README.litmus-test.md //! [examples]: https://github.com/messense/dav-server-rs/tree/main/examples/ //! [PUT]: https://github.com/messense/dav-server-rs/tree/main/doc/Apache-PUT-with-Content-Range.md //! [PATCH]: https://github.com/messense/dav-server-rs/tree/main/doc/SABREDAV-partialupdate.md //! [hyper]: https://hyper.rs/ //! [warp]: https://crates.io/crates/warp //! [actix-web]: https://actix.rs/ #![cfg_attr(docsrs, feature(doc_cfg))] #[macro_use] extern crate log; mod async_stream; mod conditional; #[cfg(any(feature = "caldav", feature = "carddav"))] pub mod dav_filters; mod davhandler; mod davheaders; mod errors; #[cfg(any(docsrs, feature = "caldav"))] #[cfg_attr(docsrs, doc(cfg(feature = "caldav")))] mod handle_caldav; #[cfg(any(docsrs, feature = "carddav"))] #[cfg_attr(docsrs, doc(cfg(feature = "carddav")))] mod handle_carddav; mod handle_copymove; mod handle_delete; mod handle_gethead; mod handle_lock; mod handle_mkcol; mod handle_options; mod handle_props; mod handle_put; #[cfg(any(docsrs, feature = "localfs"))] #[cfg_attr(docsrs, doc(cfg(feature = "localfs")))] mod localfs_macos; #[cfg(any(docsrs, feature = "localfs"))] #[cfg_attr(docsrs, doc(cfg(feature = "localfs")))] mod localfs_windows; mod multierror; mod tree; mod util; mod voidfs; mod xmltree_ext; pub mod body; #[cfg(any(docsrs, feature = "caldav"))] #[cfg_attr(docsrs, doc(cfg(feature = "caldav")))] pub mod caldav; #[cfg(any(docsrs, feature = "carddav"))] #[cfg_attr(docsrs, doc(cfg(feature = "carddav")))] pub mod carddav; pub mod davpath; pub mod fakels; pub mod fs; #[cfg(any(docsrs, feature = "localfs"))] #[cfg_attr(docsrs, doc(cfg(feature = "localfs")))] pub mod localfs; pub mod ls; #[cfg(any(docsrs, feature = "memfs"))] #[cfg_attr(docsrs, doc(cfg(feature = "memfs")))] pub mod memfs; pub mod memls; #[cfg(any(docsrs, feature = "actix-compat"))] #[cfg_attr(docsrs, doc(cfg(feature = "actix-compat")))] pub mod actix; #[cfg(any(docsrs, feature = "warp-compat"))] #[cfg_attr(docsrs, doc(cfg(feature = "warp-compat")))] pub mod warp; pub(crate) use crate::davhandler::DavInner; pub(crate) use crate::errors::{DavError, DavResult}; pub(crate) use crate::fs::*; pub use crate::davhandler::{DavConfig, DavHandler}; pub use crate::util::{DavMethod, DavMethodSet}; dav-server-0.11.0/src/localfs.rs000064400000000000000000000640311046102023000145450ustar 00000000000000//! Local filesystem access. //! //! This implementation is stateless. So the easiest way to use it //! is to create a new instance in your handler every time //! you need one. use std::any::Any; use std::collections::VecDeque; use std::future::{self, Future}; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::mem; #[cfg(unix)] use std::os::unix::{ ffi::OsStrExt, fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt}, }; #[cfg(target_os = "windows")] use std::os::windows::prelude::*; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::pin::pin; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use bytes::{Buf, Bytes, BytesMut}; use futures_util::{FutureExt, Stream, future::BoxFuture}; use tokio::task; use libc; use reflink_copy::reflink_or_copy; use crate::davpath::DavPath; use crate::fs::*; use crate::localfs_macos::DUCacheBuilder; // Run some code via block_in_place() or spawn_blocking(). // // There's also a method on LocalFs for this, use the freestanding // function if you do not want the fs_access_guard() closure to be used. #[cfg(feature = "localfs")] #[inline] async fn blocking(func: F) -> R where F: FnOnce() -> R, F: Send + 'static, R: Send + 'static, { match tokio::runtime::Handle::current().runtime_flavor() { tokio::runtime::RuntimeFlavor::MultiThread => task::block_in_place(func), _ => task::spawn_blocking(func).await.unwrap(), } } #[derive(Debug, Clone)] struct LocalFsMetaData(std::fs::Metadata); /// Local Filesystem implementation. #[derive(Clone)] pub struct LocalFs { pub(crate) inner: Arc, } // inner struct. pub(crate) struct LocalFsInner { pub basedir: PathBuf, #[allow(dead_code)] pub public: bool, pub case_insensitive: bool, pub macos: bool, pub is_file: bool, pub fs_access_guard: Option Box + Send + Sync + 'static>>, } #[derive(Debug)] struct LocalFsFile { file: Option, buf: BytesMut, } struct LocalFsReadDir { fs: LocalFs, do_meta: ReadDirMeta, buffer: VecDeque>, dir_cache: Option, iterator: Option, fut: Option>, } // a DirEntry either already has the metadata available, or a handle // to the filesystem so it can call fs.blocking() enum Meta { Data(io::Result), Fs(LocalFs), } /// Helper function to create directory on basedir #[allow(unused)] fn helper_create_directory(basedir: &Path, _new_dir_name: &str) { let new_path = basedir.join(_new_dir_name); std::fs::create_dir_all(&new_path) .expect("Failed to create default CalDAV directory; verify that 'basedir' is correct."); } // Items from the readdir stream. struct LocalFsDirEntry { meta: Meta, entry: std::fs::DirEntry, } impl LocalFs { /// Create a new LocalFs DavFileSystem, serving "base". /// /// If "public" is set to true, all files and directories created will be /// publically readable (mode 644/755), otherwise they will be private /// (mode 600/700). Umask still overrides this. /// /// If "case_insensitive" is set to true, all filesystem lookups will /// be case insensitive. Note that this has a _lot_ of overhead! pub fn new>( base: P, public: bool, case_insensitive: bool, macos: bool, ) -> Box { let basedir = base.as_ref().to_path_buf(); #[cfg(feature = "caldav")] helper_create_directory(&basedir, crate::caldav::DEFAULT_CALDAV_NAME); #[cfg(feature = "carddav")] helper_create_directory(&basedir, crate::carddav::DEFAULT_CARDDAV_NAME); let inner = LocalFsInner { basedir, public, macos, case_insensitive, is_file: false, fs_access_guard: None, }; Box::new({ LocalFs { inner: Arc::new(inner), } }) } /// Create a new LocalFs DavFileSystem, serving "file". /// /// This is like `new()`, but it always serves this single file. /// The request path is ignored. pub fn new_file>(file: P, public: bool) -> Box { let inner = LocalFsInner { basedir: file.as_ref().to_path_buf(), public, macos: false, case_insensitive: false, is_file: true, fs_access_guard: None, }; Box::new({ LocalFs { inner: Arc::new(inner), } }) } // Like new() but pass in a fs_access_guard hook. #[doc(hidden)] pub fn new_with_fs_access_guard>( base: P, public: bool, case_insensitive: bool, macos: bool, fs_access_guard: Option Box + Send + Sync + 'static>>, ) -> Box { let basedir = base.as_ref().to_path_buf(); #[cfg(feature = "caldav")] helper_create_directory(&basedir, crate::caldav::DEFAULT_CALDAV_NAME); #[cfg(feature = "carddav")] helper_create_directory(&basedir, crate::carddav::DEFAULT_CARDDAV_NAME); let inner = LocalFsInner { basedir, public, macos, case_insensitive, is_file: false, fs_access_guard, }; Box::new({ LocalFs { inner: Arc::new(inner), } }) } fn fspath_dbg(&self, path: &DavPath) -> PathBuf { let mut pathbuf = self.inner.basedir.clone(); if !self.inner.is_file { pathbuf.push(path.as_rel_ospath()); } pathbuf } fn fspath(&self, path: &DavPath) -> PathBuf { if self.inner.case_insensitive { crate::localfs_windows::resolve(&self.inner.basedir, path) } else { let mut pathbuf = self.inner.basedir.clone(); if !self.inner.is_file { pathbuf.push(path.as_rel_ospath()); } pathbuf } } // threadpool::blocking() adapter, also runs the before/after hooks. #[doc(hidden)] pub async fn blocking(&self, func: F) -> R where F: FnOnce() -> R + Send + 'static, R: Send + 'static, { let this = self.clone(); blocking(move || { let _guard = this.inner.fs_access_guard.as_ref().map(|f| f()); func() }) .await } } // This implementation is basically a bunch of boilerplate to // wrap the std::fs call in self.blocking() calls. impl DavFileSystem for LocalFs { fn metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'a, Box> { async move { if let Some(meta) = self.is_virtual(davpath) { return Ok(meta); } let path = self.fspath(davpath); if self.is_notfound(&path) { return Err(FsError::NotFound); } self.blocking(move || match std::fs::metadata(path) { Ok(meta) => Ok(Box::new(LocalFsMetaData(meta)) as Box), Err(e) => Err(e.into()), }) .await } .boxed() } fn symlink_metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'a, Box> { async move { if let Some(meta) = self.is_virtual(davpath) { return Ok(meta); } let path = self.fspath(davpath); if self.is_notfound(&path) { return Err(FsError::NotFound); } self.blocking(move || match std::fs::symlink_metadata(path) { Ok(meta) => Ok(Box::new(LocalFsMetaData(meta)) as Box), Err(e) => Err(e.into()), }) .await } .boxed() } // read_dir is a bit more involved - but not much - than a simple wrapper, // because it returns a stream. fn read_dir<'a>( &'a self, davpath: &'a DavPath, meta: ReadDirMeta, ) -> FsFuture<'a, FsStream>> { async move { trace!("FS: read_dir {:?}", self.fspath_dbg(davpath)); let path = self.fspath(davpath); let path2 = path.clone(); let iter = self.blocking(move || std::fs::read_dir(&path)).await; match iter { Ok(iterator) => { let strm = LocalFsReadDir { fs: self.clone(), do_meta: meta, buffer: VecDeque::new(), dir_cache: self.dir_cache_builder(path2), iterator: Some(iterator), fut: None, }; Ok(Box::pin(strm) as FsStream>) } Err(e) => Err(e.into()), } } .boxed() } fn open<'a>( &'a self, path: &'a DavPath, options: OpenOptions, ) -> FsFuture<'a, Box> { async move { trace!("FS: open {:?}", self.fspath_dbg(path)); if self.is_forbidden(path) { return Err(FsError::Forbidden); } #[cfg(unix)] let mode = if self.inner.public { 0o644 } else { 0o600 }; let path = self.fspath(path); self.blocking(move || { #[cfg(unix)] let res = std::fs::OpenOptions::new() .read(options.read) .write(options.write) .append(options.append) .truncate(options.truncate) .create(options.create) .create_new(options.create_new) .mode(mode) .open(path); #[cfg(windows)] let res = std::fs::OpenOptions::new() .read(options.read) .write(options.write) .append(options.append) .truncate(options.truncate) .create(options.create) .create_new(options.create_new) .open(path); match res { Ok(file) => Ok(Box::new(LocalFsFile { file: Some(file), buf: BytesMut::new(), }) as Box), Err(e) => Err(e.into()), } }) .await } .boxed() } fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> { async move { trace!("FS: create_dir {:?}", self.fspath_dbg(path)); if self.is_forbidden(path) { return Err(FsError::Forbidden); } #[cfg(unix)] let mode = if self.inner.public { 0o755 } else { 0o700 }; let path = self.fspath(path); self.blocking(move || { #[cfg(unix)] { std::fs::DirBuilder::new() .mode(mode) .create(path) .map_err(|e| e.into()) } #[cfg(windows)] { std::fs::DirBuilder::new() .create(path) .map_err(|e| e.into()) } }) .await } .boxed() } fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> { async move { trace!("FS: remove_dir {:?}", self.fspath_dbg(path)); let path = self.fspath(path); self.blocking(move || std::fs::remove_dir(path).map_err(|e| e.into())) .await } .boxed() } fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> { async move { trace!("FS: remove_file {:?}", self.fspath_dbg(path)); if self.is_forbidden(path) { return Err(FsError::Forbidden); } let path = self.fspath(path); self.blocking(move || std::fs::remove_file(path).map_err(|e| e.into())) .await } .boxed() } fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> { async move { trace!( "FS: rename {:?} {:?}", self.fspath_dbg(from), self.fspath_dbg(to) ); if self.is_forbidden(from) || self.is_forbidden(to) { return Err(FsError::Forbidden); } let frompath = self.fspath(from); let topath = self.fspath(to); self.blocking(move || { match std::fs::rename(&frompath, &topath) { Ok(v) => Ok(v), Err(e) => { // webdav allows a rename from a directory to a file. // note that this check is racy, and I'm not quite sure what // we should do if the source is a symlink. anyway ... if e.raw_os_error() == Some(libc::ENOTDIR) && frompath.is_dir() { // remove and try again. let _ = std::fs::remove_file(&topath); std::fs::rename(frompath, topath).map_err(|e| e.into()) } else { Err(e.into()) } } } }) .await } .boxed() } fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> { async move { trace!( "FS: copy {:?} {:?}", self.fspath_dbg(from), self.fspath_dbg(to) ); if self.is_forbidden(from) || self.is_forbidden(to) { return Err(FsError::Forbidden); } let path_from = self.fspath(from); let path_to = self.fspath(to); match self .blocking(move || reflink_or_copy(path_from, path_to)) .await { Ok(_) => Ok(()), Err(e) => { debug!( "copy({:?}, {:?}) failed: {}", self.fspath_dbg(from), self.fspath_dbg(to), e ); Err(e.into()) } } } .boxed() } } // read_batch() result. struct ReadDirBatch { iterator: Option, buffer: VecDeque>, } // Read the next batch of LocalFsDirEntry structs (up to 256). // This is sync code, must be run in `blocking()`. fn read_batch( iterator: Option, fs: LocalFs, do_meta: ReadDirMeta, ) -> ReadDirBatch { let mut buffer = VecDeque::new(); let mut iterator = match iterator { Some(i) => i, None => { return ReadDirBatch { buffer, iterator: None, }; } }; let _guard = match do_meta { ReadDirMeta::None => None, _ => fs.inner.fs_access_guard.as_ref().map(|f| f()), }; for _ in 0..256 { match iterator.next() { Some(Ok(entry)) => { let meta = match do_meta { ReadDirMeta::Data => Meta::Data(std::fs::metadata(entry.path())), ReadDirMeta::DataSymlink => Meta::Data(entry.metadata()), ReadDirMeta::None => Meta::Fs(fs.clone()), }; let d = LocalFsDirEntry { meta, entry }; buffer.push_back(Ok(d)) } Some(Err(e)) => { buffer.push_back(Err(e)); break; } None => break, } } ReadDirBatch { buffer, iterator: Some(iterator), } } impl LocalFsReadDir { // Create a future that calls read_batch(). // // The 'iterator' is moved into the future, and returned when it completes, // together with a list of directory entries. fn read_batch(&mut self) -> BoxFuture<'static, ReadDirBatch> { let iterator = self.iterator.take(); let fs = self.fs.clone(); let do_meta = self.do_meta; let fut: BoxFuture = blocking(move || read_batch(iterator, fs, do_meta)).boxed(); fut } } // The stream implementation tries to be smart and batch I/O operations impl Stream for LocalFsReadDir { type Item = FsResult>; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = Pin::into_inner(self); // If the buffer is empty, fill it. if this.buffer.is_empty() { // If we have no pending future, create one. if this.fut.is_none() { if this.iterator.is_none() { return Poll::Ready(None); } this.fut = Some(this.read_batch()); } // Poll the future. let mut fut = pin!(this.fut.as_mut().unwrap()); match Pin::new(&mut fut).poll(cx) { Poll::Ready(batch) => { this.fut.take(); if let Some(ref mut nb) = this.dir_cache { batch.buffer.iter().for_each(|e| { if let Ok(e) = e { nb.add(e.entry.file_name()); } }); } this.buffer = batch.buffer; this.iterator = batch.iterator; } Poll::Pending => return Poll::Pending, } } // we filled the buffer, now pop from the buffer. match this.buffer.pop_front() { Some(Ok(item)) => Poll::Ready(Some(Ok(Box::new(item)))), Some(Err(err)) => { // fuse the iterator. this.iterator.take(); // finish the cache. if let Some(ref mut nb) = this.dir_cache { nb.finish(); } // return error of stream. Poll::Ready(Some(Err(err.into()))) } None => { // fuse the iterator. this.iterator.take(); // finish the cache. if let Some(ref mut nb) = this.dir_cache { nb.finish(); } // return end-of-stream. Poll::Ready(None) } } } } enum Is { File, Dir, Symlink, } impl LocalFsDirEntry { async fn is_a(&self, is: Is) -> FsResult { match self.meta { Meta::Data(Ok(ref meta)) => Ok(match is { Is::File => meta.file_type().is_file(), Is::Dir => meta.file_type().is_dir(), Is::Symlink => meta.file_type().is_symlink(), }), Meta::Data(Err(ref e)) => Err(e.into()), Meta::Fs(ref fs) => { let fullpath = self.entry.path(); let ft = fs .blocking(move || std::fs::metadata(&fullpath)) .await? .file_type(); Ok(match is { Is::File => ft.is_file(), Is::Dir => ft.is_dir(), Is::Symlink => ft.is_symlink(), }) } } } } impl DavDirEntry for LocalFsDirEntry { fn metadata(&'_ self) -> FsFuture<'_, Box> { match self.meta { Meta::Data(ref meta) => { let m = match meta { Ok(meta) => Ok(Box::new(LocalFsMetaData(meta.clone())) as Box), Err(e) => Err(e.into()), }; Box::pin(future::ready(m)) } Meta::Fs(ref fs) => { let fullpath = self.entry.path(); fs.blocking(move || match std::fs::metadata(&fullpath) { Ok(meta) => Ok(Box::new(LocalFsMetaData(meta)) as Box), Err(e) => Err(e.into()), }) .boxed() } } } #[cfg(unix)] fn name(&self) -> Vec { self.entry.file_name().as_bytes().to_vec() } #[cfg(windows)] fn name(&self) -> Vec { self.entry.file_name().to_str().unwrap().as_bytes().to_vec() } fn is_dir(&'_ self) -> FsFuture<'_, bool> { Box::pin(self.is_a(Is::Dir)) } fn is_file(&'_ self) -> FsFuture<'_, bool> { Box::pin(self.is_a(Is::File)) } fn is_symlink(&'_ self) -> FsFuture<'_, bool> { Box::pin(self.is_a(Is::Symlink)) } } impl DavFile for LocalFsFile { fn metadata(&'_ mut self) -> FsFuture<'_, Box> { async move { let file = self.file.take().unwrap(); let (meta, file) = blocking(move || (file.metadata(), file)).await; self.file = Some(file); Ok(Box::new(LocalFsMetaData(meta?)) as Box) } .boxed() } fn write_bytes(&'_ mut self, buf: Bytes) -> FsFuture<'_, ()> { async move { let mut file = self.file.take().unwrap(); let (res, file) = blocking(move || (file.write_all(&buf), file)).await; self.file = Some(file); res.map_err(|e| e.into()) } .boxed() } fn write_buf(&'_ mut self, mut buf: Box) -> FsFuture<'_, ()> { async move { let mut file = self.file.take().unwrap(); let (res, file) = blocking(move || { while buf.remaining() > 0 { let n = match file.write(buf.chunk()) { Ok(n) => n, Err(e) => return (Err(e), file), }; buf.advance(n); } (Ok(()), file) }) .await; self.file = Some(file); res.map_err(|e| e.into()) } .boxed() } fn read_bytes(&'_ mut self, count: usize) -> FsFuture<'_, Bytes> { async move { let mut file = self.file.take().unwrap(); let mut buf = mem::take(&mut self.buf); let (res, file, buf) = blocking(move || { buf.reserve(count); let res = unsafe { buf.set_len(count); file.read(&mut buf).map(|n| { buf.set_len(n); buf.split().freeze() }) }; (res, file, buf) }) .await; self.file = Some(file); self.buf = buf; res.map_err(|e| e.into()) } .boxed() } fn seek(&'_ mut self, pos: SeekFrom) -> FsFuture<'_, u64> { async move { let mut file = self.file.take().unwrap(); let (res, file) = blocking(move || (file.seek(pos), file)).await; self.file = Some(file); res.map_err(|e| e.into()) } .boxed() } fn flush(&'_ mut self) -> FsFuture<'_, ()> { async move { let mut file = self.file.take().unwrap(); let (res, file) = blocking(move || (file.flush(), file)).await; self.file = Some(file); res.map_err(|e| e.into()) } .boxed() } } impl DavMetaData for LocalFsMetaData { fn len(&self) -> u64 { self.0.len() } fn created(&self) -> FsResult { self.0.created().map_err(|e| e.into()) } fn modified(&self) -> FsResult { self.0.modified().map_err(|e| e.into()) } fn accessed(&self) -> FsResult { self.0.accessed().map_err(|e| e.into()) } #[cfg(unix)] fn status_changed(&self) -> FsResult { Ok(UNIX_EPOCH + Duration::new(self.0.ctime() as u64, 0)) } #[cfg(windows)] fn status_changed(&self) -> FsResult { Ok(UNIX_EPOCH + Duration::from_nanos(self.0.creation_time() - 116444736000000000)) } fn is_dir(&self) -> bool { self.0.is_dir() } fn is_file(&self) -> bool { self.0.is_file() } fn is_symlink(&self) -> bool { self.0.file_type().is_symlink() } #[cfg(feature = "caldav")] fn is_calendar(&self, path: &DavPath) -> bool { crate::caldav::is_path_in_caldav_directory(path) } #[cfg(feature = "carddav")] fn is_addressbook(&self, path: &DavPath) -> bool { crate::carddav::is_path_in_carddav_directory(path) } #[cfg(unix)] fn executable(&self) -> FsResult { if self.0.is_file() { return Ok((self.0.permissions().mode() & 0o100) > 0); } Err(FsError::NotImplemented) } #[cfg(windows)] fn executable(&self) -> FsResult { // FIXME: implement Err(FsError::NotImplemented) } // same as the default apache etag. #[cfg(unix)] fn etag(&self) -> Option { let modified = self.0.modified().ok()?; let t = modified.duration_since(UNIX_EPOCH).ok()?; let t = t.as_secs() * 1000000 + t.subsec_nanos() as u64 / 1000; if self.is_file() { Some(format!("{:x}-{:x}-{:x}", self.0.ino(), self.0.len(), t)) } else { Some(format!("{:x}-{:x}", self.0.ino(), t)) } } // same as the default apache etag. #[cfg(windows)] fn etag(&self) -> Option { let modified = self.0.modified().ok()?; let t = modified.duration_since(UNIX_EPOCH).ok()?; let t = t.as_secs() * 1000000 + t.subsec_nanos() as u64 / 1000; if self.is_file() { Some(format!("{:x}-{:x}", self.0.len(), t)) } else { Some(format!("{:x}", t)) } } } dav-server-0.11.0/src/localfs_macos.rs000064400000000000000000000230661046102023000157320ustar 00000000000000// Optimizations for macOS and the macOS finder. // // - after it reads a directory, macOS likes to do a PROPSTAT of all // files in the directory with "._" prefixed. so after each PROPSTAT // with Depth: 1 we keep a cache of "._" files we've seen, so that // we can easily tell which ones did _not_ exist. // - deny existence of ".localized" files // - fake a ".metadata_never_index" in the root // - fake a ".ql_disablethumbnails" file in the root. // use std::ffi::OsString; use std::num::NonZeroUsize; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use std::sync::atomic::{AtomicUsize, Ordering}; use std::thread; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use lru::LruCache; use parking_lot::Mutex; use crate::davpath::DavPath; use crate::fs::*; use crate::localfs::LocalFs; const DU_CACHE_ENTRIES: usize = 4096; const DU_CACHE_MAX_AGE: u64 = 60; const DU_CACHE_SLEEP_MS: u64 = 10037; static DU_CACHE: LazyLock = LazyLock::new(|| DUCache::new(DU_CACHE_ENTRIES)); static DIR_ID: AtomicUsize = AtomicUsize::new(1); // Dot underscore cache entry. struct Entry { // Time the entry in the cache was created. time: SystemTime, // Modification time of the parent directory. dir_modtime: SystemTime, // Unique ID of the parent entry. dir_id: usize, } // Dot underscore cache. struct DUCache { cache: Mutex>, } impl DUCache { // return a new instance. fn new(size: usize) -> DUCache { thread::spawn(move || { loop { // House keeping. Every 10 seconds, remove entries older than // DU_CACHE_MAX_AGE seconds from the LRU cache. thread::sleep(Duration::from_millis(DU_CACHE_SLEEP_MS)); { let mut cache = DU_CACHE.cache.lock(); let now = SystemTime::now(); while let Some((_k, e)) = cache.peek_lru() { if let Ok(age) = now.duration_since(e.time) { trace!(target: "webdav_cache", "DUCache: purge check {_k:?}"); if age.as_secs() <= DU_CACHE_MAX_AGE { break; } if let Some((_k, _)) = cache.pop_lru() { trace!(target: "webdav_cache", "DUCache: purging {:?} (age {})", _k, age.as_secs()); } else { break; } } else { break; } } } } }); DUCache { cache: Mutex::new(LruCache::new(NonZeroUsize::new(size).unwrap())), } } // Lookup a "._filename" entry in the cache. If we are sure the path // does _not_ exist, return `true`. // // Note that it's assumed the file_name() DOES start with "._". fn negative(&self, path: &PathBuf) -> bool { // parent directory must be present in the cache. let mut dir = match path.parent() { Some(d) => d.to_path_buf(), None => return false, }; dir.push("."); let (dir_id, dir_modtime) = { let cache = self.cache.lock(); match cache.peek(&dir) { Some(t) => (t.dir_id, t.dir_modtime), None => { trace!(target: "webdav_cache", "DUCache::negative({path:?}): parent not in cache"); return false; } } }; // Get the metadata of the parent to see if it changed. // This is pretty cheap, since it's most likely in the kernel cache. let valid = match std::fs::metadata(&dir) { Ok(m) => m.modified().map(|m| m == dir_modtime).unwrap_or(false), Err(_) => false, }; let mut cache = self.cache.lock(); if !valid { trace!(target: "webdav_cache", "DUCache::negative({path:?}): parent in cache but stale"); cache.pop(&dir); return false; } // Now if there is _no_ entry in the cache for this file, // or it is not valid (different timestamp), it did not exist // the last time we did a readdir(). match cache.peek(path) { Some(t) => { trace!(target: "webdav_cache", "DUCache::negative({:?}): in cache, valid: {}", path, t.dir_id != dir_id); t.dir_id != dir_id } None => { trace!(target: "webdav_cache", "DUCache::negative({path:?}): not in cache"); true } } } } // Storage for the entries of one dir while we're collecting them. #[derive(Default)] pub(crate) struct DUCacheBuilder { dir: PathBuf, entries: Vec, done: bool, } impl DUCacheBuilder { // return a new instance. pub fn start(dir: PathBuf) -> DUCacheBuilder { DUCacheBuilder { dir, entries: Vec::new(), done: false, } } // add a filename to the list we have #[cfg(unix)] pub fn add(&mut self, filename: OsString) { if let Some(f) = Path::new(&filename).file_name() && f.as_bytes().starts_with(b"._") { self.entries.push(filename); } } // add a filename to the list we have #[cfg(windows)] pub fn add(&mut self, filename: OsString) { if let Some(f) = Path::new(&filename).file_name() && f.to_str().unwrap().as_bytes().starts_with(b"._") { self.entries.push(filename); } } // Process the "._" files we collected. // // We add all the "._" files we saw in the directory, and the // directory itself (with "/." added). pub fn finish(&mut self) { if self.done { return; } self.done = true; // Get parent directory modification time. let meta = match std::fs::metadata(&self.dir) { Ok(m) => m, Err(_) => return, }; let dir_modtime = match meta.modified() { Ok(t) => t, Err(_) => return, }; let dir_id = DIR_ID.fetch_add(1, Ordering::SeqCst); let now = SystemTime::now(); let mut cache = DU_CACHE.cache.lock(); // Add "/." to directory and store it. let mut path = self.dir.clone(); path.push("."); let entry = Entry { time: now, dir_modtime, dir_id, }; cache.put(path, entry); // Now add the "._" files. for filename in self.entries.drain(..) { // create full path and add it to the cache. let mut path = self.dir.clone(); path.push(filename); let entry = Entry { time: now, dir_modtime, dir_id, }; cache.put(path, entry); } } } // Fake metadata for an empty file. #[derive(Debug, Clone)] struct EmptyMetaData; impl DavMetaData for EmptyMetaData { fn len(&self) -> u64 { 0 } fn is_dir(&self) -> bool { false } fn modified(&self) -> FsResult { // Tue May 30 04:00:00 CEST 2000 Ok(UNIX_EPOCH + Duration::new(959652000, 0)) } fn created(&self) -> FsResult { self.modified() } #[cfg(feature = "caldav")] fn is_calendar(&self, _: &DavPath) -> bool { false } #[cfg(feature = "carddav")] fn is_addressbook(&self, _: &DavPath) -> bool { false } } impl LocalFs { // Is this a virtualfile ? #[inline] pub(crate) fn is_virtual(&self, path: &DavPath) -> Option> { if !self.inner.macos { return None; } match path.as_bytes() { b"/.metadata_never_index" => {} b"/.ql_disablethumbnails" => {} _ => return None, } Some(Box::new(EmptyMetaData {})) } // This file can never exist. #[inline] pub(crate) fn is_forbidden(&self, path: &DavPath) -> bool { if !self.inner.macos { return false; } match path.as_bytes() { b"/.metadata_never_index" => return true, b"/.ql_disablethumbnails" => return true, _ => {} } path.file_name_bytes() == b".localized" } // File might not exists because of negative cache entry. #[cfg(unix)] #[inline] pub(crate) fn is_notfound(&self, path: &PathBuf) -> bool { if !self.inner.macos { return false; } match path.file_name().map(|p| p.as_bytes()) { Some(b".localized") => true, Some(name) if name.starts_with(b"._") => DU_CACHE.negative(path), _ => false, } } // File might not exists because of negative cache entry. #[cfg(windows)] #[inline] pub(crate) fn is_notfound(&self, path: &PathBuf) -> bool { if !self.inner.macos { return false; } match path.file_name().map(|p| p.to_str().unwrap().as_bytes()) { Some(b".localized") => true, Some(name) if name.starts_with(b"._") => DU_CACHE.negative(path), _ => false, } } // Return a "directory cache builder". #[inline] pub(crate) fn dir_cache_builder(&self, path: PathBuf) -> Option { if self.inner.macos { Some(DUCacheBuilder::start(path)) } else { None } } } dav-server-0.11.0/src/localfs_windows.rs000064400000000000000000000160221046102023000163140ustar 00000000000000// Optimizations for windows and the windows webdav mini-redirector. // // The main thing here is case-insensitive path lookups, // and caching that. // use std::ffi::{OsStr, OsString}; use std::fs; use std::io::ErrorKind; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use std::thread; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use lru::LruCache; use parking_lot::Mutex; use crate::davpath::DavPath; const CACHE_ENTRIES: usize = 4096; const CACHE_MAX_AGE: u64 = 15 * 60; const CACHE_SLEEP_MS: u64 = 30059; static CACHE: LazyLock = LazyLock::new(|| Cache::new(CACHE_ENTRIES)); // Do a case-insensitive path lookup. pub(crate) fn resolve(base: impl Into, path: &DavPath) -> PathBuf { let base = base.into(); let path = path.as_rel_ospath(); // must be rooted, and valid UTF-8. let mut fullpath = base.clone(); fullpath.push(path); if !fullpath.has_root() || fullpath.to_str().is_none() { return fullpath; } // must have a parent. let parent = match fullpath.parent() { Some(p) => p, None => return fullpath, }; // deref in advance: first LazyLock, then Arc. let cache = &*CACHE; // In the cache? if let Some((path, _)) = cache.get(&fullpath) { return path; } // if the file exists, fine. if fullpath.metadata().is_ok() { return fullpath; } // we need the path as a list of segments. let segs = path.iter().collect::>(); if segs.is_empty() { return fullpath; } // if the parent exists, do a lookup there straight away // instead of starting from the root. let (parent, parent_exists) = if segs.len() > 1 { match cache.get(parent) { Some((path, _)) => (path, true), None => { let exists = parent.exists(); if exists { cache.insert(parent); } (parent.to_path_buf(), exists) } } } else { (parent.to_path_buf(), true) }; if parent_exists { let (newpath, stop) = lookup(parent, segs[segs.len() - 1], true); if !stop { cache.insert(&newpath); } return newpath; } // start from the root, then add segments one by one. let mut stop = false; let mut newpath = base; let lastseg = segs.len() - 1; for (idx, seg) in segs.into_iter().enumerate() { if !stop { if idx == lastseg { // Save the path leading up to this file or dir. cache.insert(&newpath); } let (n, s) = lookup(newpath, seg, false); newpath = n; stop = s; } else { newpath.push(seg); } } if !stop { // resolved succesfully. save in cache. cache.insert(&newpath); } newpath } // lookup a filename in a directory in a case insensitive way. fn lookup(mut path: PathBuf, seg: &OsStr, no_init_check: bool) -> (PathBuf, bool) { // does it exist as-is? let mut path2 = path.clone(); path2.push(seg); if !no_init_check { match path2.metadata() { Ok(_) => return (path2, false), Err(ref e) if e.kind() != ErrorKind::NotFound => { // stop on errors other than "NotFound". return (path2, true); } Err(_) => {} } } // first, lowercase filename. let filename = match seg.to_str() { Some(s) => s.to_lowercase(), None => return (path2, true), }; // we have to read the entire directory. let dir = match path.read_dir() { Ok(dir) => dir, Err(_) => return (path2, true), }; for entry in dir.into_iter() { let entry = match entry { Ok(e) => e, Err(_) => continue, }; let entry_name = entry.file_name(); let name = match entry_name.to_str() { Some(n) => n, None => continue, }; if name.to_lowercase() == filename { path.push(name); return (path, false); } } (path2, true) } // The cache stores a mapping of lowercased path -> actual path. pub struct Cache { cache: Mutex>, } #[derive(Clone)] struct Entry { // Full case-sensitive pathname. path: PathBuf, // Unix timestamp. time: u64, } // helper fn pathbuf_to_lowercase(path: PathBuf) -> PathBuf { let s = match OsString::from(path).into_string() { Ok(s) => OsString::from(s.to_lowercase()), Err(s) => s, }; PathBuf::from(s) } impl Cache { pub fn new(size: usize) -> Cache { thread::spawn(move || { // House keeping. Every 30 seconds, remove entries older than // CACHE_MAX_AGE seconds from the LRU cache. loop { thread::sleep(Duration::from_millis(CACHE_SLEEP_MS)); if let Ok(d) = SystemTime::now().duration_since(UNIX_EPOCH) { let now = d.as_secs(); let mut cache = CACHE.cache.lock(); while let Some((_k, e)) = cache.peek_lru() { trace!(target: "webdav_cache", "Cache: purge check: {_k:?}"); if e.time + CACHE_MAX_AGE > now { break; } let _age = now - e.time; if let Some((_k, _)) = cache.pop_lru() { trace!(target: "webdav_cache", "Cache: purging {_k:?} (age {_age})"); } else { break; } } drop(cache); } } }); Cache { cache: Mutex::new(LruCache::new(NonZeroUsize::new(size).unwrap())), } } // Insert an entry into the cache. pub fn insert(&self, path: &Path) { let lc_path = pathbuf_to_lowercase(PathBuf::from(path)); if let Ok(d) = SystemTime::now().duration_since(UNIX_EPOCH) { let e = Entry { path: PathBuf::from(path), time: d.as_secs(), }; let mut cache = self.cache.lock(); cache.put(lc_path, e); } } // Get an entry from the cache, and validate it. If it's valid // return the actual pathname and metadata. If it's invalid remove // it from the cache and return None. pub fn get(&self, path: &Path) -> Option<(PathBuf, fs::Metadata)> { // First lowercase the entire path. let lc_path = pathbuf_to_lowercase(PathBuf::from(path)); // Lookup. let e = { let mut cache = self.cache.lock(); cache.get(&lc_path)?.clone() }; // Found, validate. match fs::metadata(&e.path) { Err(_) => { let mut cache = self.cache.lock(); cache.pop(&lc_path); None } Ok(m) => Some((e.path, m)), } } } dav-server-0.11.0/src/ls.rs000064400000000000000000000053201046102023000135340ustar 00000000000000//! Contains the structs and traits that define a `locksystem` backend. //! //! Note that the methods DO NOT return futures, they are synchronous. //! This is because currently only two locksystems exist, `MemLs` and `FakeLs`. //! Both of them do not do any I/O, all methods return instantly. //! //! If ever a locksystem gets built that does I/O (to a filesystem, //! a database, or over the network) we'll need to revisit this. //! use crate::davpath::DavPath; use std::fmt::Debug; use std::future::Future; use std::pin::Pin; use std::time::{Duration, SystemTime}; use dyn_clone::{DynClone, clone_trait_object}; use xmltree::Element; /// Type of the locks returned by DavLockSystem methods. #[derive(Debug, Clone)] pub struct DavLock { /// Token. pub token: String, /// Path/ pub path: Box, /// Principal. pub principal: Option, /// Owner. pub owner: Option>, /// When the lock turns stale (absolute). pub timeout_at: Option, /// When the lock turns stale (relative). pub timeout: Option, /// Shared. pub shared: bool, /// Deep. pub deep: bool, } pub type LsFuture<'a, T> = Pin + Send + 'a>>; /// The trait that defines a locksystem. pub trait DavLockSystem: Debug + Send + Sync + DynClone { /// Lock a node. Returns `Ok(new_lock)` if succeeded, /// or `Err(conflicting_lock)` if failed. fn lock( &'_ self, path: &DavPath, principal: Option<&str>, owner: Option<&Element>, timeout: Option, shared: bool, deep: bool, ) -> LsFuture<'_, Result>; /// Unlock a node. Returns `Ok(())` if succeeded, `Err (())` if failed /// (because lock doesn't exist) fn unlock(&'_ self, path: &DavPath, token: &str) -> LsFuture<'_, Result<(), ()>>; /// Refresh lock. Returns updated lock if succeeded. fn refresh( &'_ self, path: &DavPath, token: &str, timeout: Option, ) -> LsFuture<'_, Result>; /// Check if node is locked and if so, if we own all the locks. /// If not, returns as Err one conflicting lock. fn check( &'_ self, path: &DavPath, principal: Option<&str>, ignore_principal: bool, deep: bool, submitted_tokens: &[String], ) -> LsFuture<'_, Result<(), DavLock>>; /// Find and return all locks that cover a given path. fn discover(&'_ self, path: &DavPath) -> LsFuture<'_, Vec>; /// Delete all locks at this path and below (after MOVE or DELETE) fn delete(&'_ self, path: &DavPath) -> LsFuture<'_, Result<(), ()>>; } clone_trait_object! {DavLockSystem} dav-server-0.11.0/src/memfs.rs000064400000000000000000000450471046102023000142370ustar 00000000000000//! Simple in-memory filesystem. //! //! This implementation has state, so if you create a //! new instance in a handler(), it will be empty every time. //! //! This means you have to create the instance once, using `MemFs::new`, store //! it in your handler struct, and clone() it every time you pass //! it to the DavHandler. As a MemFs struct is just a handle, cloning is cheap. use std::collections::HashMap; use std::io::{Error, ErrorKind, SeekFrom}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; use bytes::{Buf, Bytes}; use futures_util::{ StreamExt, future, future::{BoxFuture, FutureExt}, }; use http::StatusCode; use crate::davpath::DavPath; use crate::fs::*; use crate::tree; type Tree = tree::Tree, MemFsNode>; /// Ephemeral in-memory filesystem. #[derive(Debug)] pub struct MemFs { tree: Arc>, } #[derive(Debug, Clone)] enum MemFsNode { Dir(MemFsDirNode), File(MemFsFileNode), } #[derive(Debug, Clone)] struct MemFsDirNode { props: HashMap, mtime: SystemTime, crtime: SystemTime, } #[derive(Debug, Clone)] struct MemFsFileNode { props: HashMap, mtime: SystemTime, crtime: SystemTime, data: Vec, } #[derive(Debug, Clone)] struct MemFsDirEntry { mtime: SystemTime, crtime: SystemTime, is_dir: bool, name: Vec, size: u64, } #[derive(Debug)] struct MemFsFile { tree: Arc>, node_id: u64, pos: usize, append: bool, } impl MemFs { /// Create a new "memfs" filesystem. pub fn new() -> Box { #[allow(unused_mut)] let mut tree = Tree::new(MemFsNode::new_dir()); #[cfg(feature = "caldav")] { tree.add_child( tree::ROOT_ID, b"calendars".to_vec(), MemFsNode::new_dir(), false, ) .unwrap(); } #[cfg(feature = "carddav")] { tree.add_child( tree::ROOT_ID, b"addressbooks".to_vec(), MemFsNode::new_dir(), false, ) .unwrap(); } Box::new(MemFs { tree: Arc::new(Mutex::new(tree)), }) } fn do_open( &self, tree: &mut Tree, path: &[u8], options: OpenOptions, ) -> FsResult> { let node_id = match tree.lookup(path) { Ok(n) => { if options.create_new { return Err(FsError::Exists); } n } Err(FsError::NotFound) => { if !options.create { return Err(FsError::NotFound); } let parent_id = tree.lookup_parent(path)?; tree.add_child(parent_id, file_name(path), MemFsNode::new_file(), true)? } Err(e) => return Err(e), }; let node = tree.get_node_mut(node_id).unwrap(); if node.is_dir() { return Err(FsError::Forbidden); } if options.truncate { node.as_file_mut()?.data.truncate(0); node.update_mtime(SystemTime::now()); } Ok(Box::new(MemFsFile { tree: self.tree.clone(), node_id, pos: 0, append: options.append, })) } } impl Clone for MemFs { fn clone(&self) -> Self { MemFs { tree: Arc::clone(&self.tree), } } } impl DavFileSystem for MemFs { fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box> { async move { let tree = &*self.tree.lock().unwrap(); let node_id = tree.lookup(path.as_bytes())?; let meta = tree.get_node(node_id)?.as_dirent(path.as_bytes()); Ok(Box::new(meta) as Box) } .boxed() } fn read_dir<'a>( &'a self, path: &'a DavPath, _meta: ReadDirMeta, ) -> FsFuture<'a, FsStream>> { async move { let tree = &*self.tree.lock().unwrap(); let node_id = tree.lookup(path.as_bytes())?; if !tree.get_node(node_id)?.is_dir() { return Err(FsError::Forbidden); } let mut v: Vec> = Vec::new(); for (name, dnode_id) in tree.get_children(node_id)? { if let Ok(node) = tree.get_node(dnode_id) { v.push(Box::new(node.as_dirent(&name))); } } let strm = futures_util::stream::iter(v).map(Ok); Ok(Box::pin(strm) as FsStream>) } .boxed() } fn open<'a>( &'a self, path: &'a DavPath, options: OpenOptions, ) -> FsFuture<'a, Box> { async move { let tree = &mut *self.tree.lock().unwrap(); self.do_open(tree, path.as_bytes(), options) } .boxed() } fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> { async move { trace!("FS: create_dir {path:?}"); let tree = &mut *self.tree.lock().unwrap(); let path = path.as_bytes(); let parent_id = tree.lookup_parent(path)?; tree.add_child(parent_id, file_name(path), MemFsNode::new_dir(), false)?; tree.get_node_mut(parent_id)? .update_mtime(SystemTime::now()); Ok(()) } .boxed() } fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> { async move { let tree = &mut *self.tree.lock().unwrap(); let parent_id = tree.lookup_parent(path.as_bytes())?; let node_id = tree.lookup(path.as_bytes())?; tree.delete_node(node_id)?; tree.get_node_mut(parent_id)? .update_mtime(SystemTime::now()); Ok(()) } .boxed() } fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> { async move { let tree = &mut *self.tree.lock().unwrap(); let parent_id = tree.lookup_parent(path.as_bytes())?; let node_id = tree.lookup(path.as_bytes())?; tree.delete_node(node_id)?; tree.get_node_mut(parent_id)? .update_mtime(SystemTime::now()); Ok(()) } .boxed() } fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> { async move { let tree = &mut *self.tree.lock().unwrap(); let node_id = tree.lookup(from.as_bytes())?; let parent_id = tree.lookup_parent(from.as_bytes())?; let dst_id = tree.lookup_parent(to.as_bytes())?; tree.move_node(node_id, dst_id, file_name(to.as_bytes()), true)?; tree.get_node_mut(parent_id)? .update_mtime(SystemTime::now()); tree.get_node_mut(dst_id)?.update_mtime(SystemTime::now()); Ok(()) } .boxed() } fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> { async move { let tree = &mut *self.tree.lock().unwrap(); // source must exist. let snode_id = tree.lookup(from.as_bytes())?; // make sure destination exists, create if needed. { let mut oo = OpenOptions::write(); oo.create = true; self.do_open(tree, to.as_bytes(), oo)?; } let dnode_id = tree.lookup(to.as_bytes())?; // copy. let mut data = (*tree.get_node_mut(snode_id)?).clone(); match data { MemFsNode::Dir(ref mut d) => d.crtime = SystemTime::now(), MemFsNode::File(ref mut f) => f.crtime = SystemTime::now(), } *tree.get_node_mut(dnode_id)? = data; Ok(()) } .boxed() } fn have_props<'a>(&'a self, _path: &'a DavPath) -> BoxFuture<'a, bool> { future::ready(true).boxed() } fn patch_props<'a>( &'a self, path: &'a DavPath, mut patch: Vec<(bool, DavProp)>, ) -> FsFuture<'a, Vec<(StatusCode, DavProp)>> { async move { let tree = &mut *self.tree.lock().unwrap(); let node_id = tree.lookup(path.as_bytes())?; let node = tree.get_node_mut(node_id)?; let props = node.get_props_mut(); let mut res = Vec::new(); for (set, p) in patch.drain(..) { let prop = cloneprop(&p); let status = if set { props.insert(propkey(&p.namespace, &p.name), p); StatusCode::OK } else { props.remove(&propkey(&p.namespace, &p.name)); // the below map was added to signify if the remove succeeded or // failed. however it seems that removing non-existant properties // always succeed, so just return success. // .map(|_| StatusCode::OK).unwrap_or(StatusCode::NOT_FOUND) StatusCode::OK }; res.push((status, prop)); } Ok(res) } .boxed() } fn get_props<'a>(&'a self, path: &'a DavPath, do_content: bool) -> FsFuture<'a, Vec> { async move { let tree = &mut *self.tree.lock().unwrap(); let node_id = tree.lookup(path.as_bytes())?; let node = tree.get_node(node_id)?; let mut res = Vec::new(); for p in node.get_props().values() { res.push(if do_content { p.clone() } else { cloneprop(p) }); } Ok(res) } .boxed() } fn get_prop<'a>(&'a self, path: &'a DavPath, prop: DavProp) -> FsFuture<'a, Vec> { async move { let tree = &mut *self.tree.lock().unwrap(); let node_id = tree.lookup(path.as_bytes())?; let node = tree.get_node(node_id)?; let p = node .get_props() .get(&propkey(&prop.namespace, &prop.name)) .ok_or(FsError::NotFound)?; p.xml.clone().ok_or(FsError::NotFound) } .boxed() } } // small helper. fn propkey(ns: &Option, name: &str) -> String { ns.to_owned().as_ref().unwrap_or(&"".to_string()).clone() + name } // small helper. fn cloneprop(p: &DavProp) -> DavProp { DavProp { name: p.name.clone(), namespace: p.namespace.clone(), prefix: p.prefix.clone(), xml: None, } } impl DavDirEntry for MemFsDirEntry { fn metadata(&'_ self) -> FsFuture<'_, Box> { let meta = (*self).clone(); Box::pin(future::ok(Box::new(meta) as Box)) } fn name(&self) -> Vec { self.name.clone() } } impl DavFile for MemFsFile { fn metadata(&'_ mut self) -> FsFuture<'_, Box> { async move { let tree = &*self.tree.lock().unwrap(); let node = tree.get_node(self.node_id)?; let meta = node.as_dirent(b""); Ok(Box::new(meta) as Box) } .boxed() } fn read_bytes(&'_ mut self, count: usize) -> FsFuture<'_, Bytes> { async move { let tree = &*self.tree.lock().unwrap(); let node = tree.get_node(self.node_id)?; let file = node.as_file()?; let curlen = file.data.len(); let mut start = self.pos; let mut end = self.pos + count; if start > curlen { start = curlen } if end > curlen { end = curlen } let cnt = end - start; self.pos += cnt; Ok(Bytes::copy_from_slice(&file.data[start..end])) } .boxed() } fn write_bytes(&'_ mut self, buf: Bytes) -> FsFuture<'_, ()> { async move { let tree = &mut *self.tree.lock().unwrap(); let node = tree.get_node_mut(self.node_id)?; let file = node.as_file_mut()?; if self.append { self.pos = file.data.len(); } let end = self.pos + buf.len(); if end > file.data.len() { file.data.resize(end, 0); } file.data[self.pos..end].copy_from_slice(&buf); self.pos = end; Ok(()) } .boxed() } fn write_buf(&'_ mut self, mut buf: Box) -> FsFuture<'_, ()> { async move { let tree = &mut *self.tree.lock().unwrap(); let node = tree.get_node_mut(self.node_id)?; let file = node.as_file_mut()?; if self.append { self.pos = file.data.len(); } let end = self.pos + buf.remaining(); if end > file.data.len() { file.data.resize(end, 0); } while buf.has_remaining() { let b = buf.chunk(); let len = b.len(); file.data[self.pos..self.pos + len].copy_from_slice(b); buf.advance(len); self.pos += len; } Ok(()) } .boxed() } fn flush(&'_ mut self) -> FsFuture<'_, ()> { future::ok(()).boxed() } fn seek(&'_ mut self, pos: SeekFrom) -> FsFuture<'_, u64> { async move { let (start, offset): (u64, i64) = match pos { SeekFrom::Start(npos) => { self.pos = npos as usize; return Ok(npos); } SeekFrom::Current(npos) => (self.pos as u64, npos), SeekFrom::End(npos) => { let tree = &*self.tree.lock().unwrap(); let node = tree.get_node(self.node_id)?; let curlen = node.as_file()?.data.len() as u64; (curlen, npos) } }; if offset < 0 { if -offset as u64 > start { return Err(Error::new(ErrorKind::InvalidInput, "invalid seek").into()); } self.pos = (start - (-offset as u64)) as usize; } else { self.pos = (start + offset as u64) as usize; } Ok(self.pos as u64) } .boxed() } } impl DavMetaData for MemFsDirEntry { fn len(&self) -> u64 { self.size } fn created(&self) -> FsResult { Ok(self.crtime) } fn modified(&self) -> FsResult { Ok(self.mtime) } fn is_dir(&self) -> bool { self.is_dir } #[cfg(feature = "caldav")] fn is_calendar(&self, path: &DavPath) -> bool { crate::caldav::is_path_in_caldav_directory(path) } #[cfg(feature = "carddav")] fn is_addressbook(&self, path: &DavPath) -> bool { crate::carddav::is_path_in_carddav_directory(path) } } impl MemFsNode { fn new_dir() -> MemFsNode { MemFsNode::Dir(MemFsDirNode { crtime: SystemTime::now(), mtime: SystemTime::now(), props: HashMap::new(), }) } fn new_file() -> MemFsNode { MemFsNode::File(MemFsFileNode { crtime: SystemTime::now(), mtime: SystemTime::now(), props: HashMap::new(), data: Vec::new(), }) } // helper to create MemFsDirEntry from a node. fn as_dirent(&self, name: &[u8]) -> MemFsDirEntry { let (is_dir, size, mtime, crtime) = match *self { MemFsNode::File(ref file) => (false, file.data.len() as u64, file.mtime, file.crtime), MemFsNode::Dir(ref dir) => (true, 0, dir.mtime, dir.crtime), }; MemFsDirEntry { name: name.to_vec(), mtime, crtime, is_dir, size, } } fn update_mtime(&mut self, tm: std::time::SystemTime) { match *self { MemFsNode::Dir(ref mut d) => d.mtime = tm, MemFsNode::File(ref mut f) => f.mtime = tm, } } fn is_dir(&self) -> bool { match *self { MemFsNode::Dir(_) => true, MemFsNode::File(_) => false, } } fn as_file(&self) -> FsResult<&MemFsFileNode> { match *self { MemFsNode::File(ref n) => Ok(n), _ => Err(FsError::Forbidden), } } fn as_file_mut(&mut self) -> FsResult<&mut MemFsFileNode> { match *self { MemFsNode::File(ref mut n) => Ok(n), _ => Err(FsError::Forbidden), } } fn get_props(&self) -> &HashMap { match *self { MemFsNode::File(ref n) => &n.props, MemFsNode::Dir(ref d) => &d.props, } } fn get_props_mut(&mut self) -> &mut HashMap { match *self { MemFsNode::File(ref mut n) => &mut n.props, MemFsNode::Dir(ref mut d) => &mut d.props, } } } trait TreeExt { fn lookup_segs(&self, segs: Vec<&[u8]>) -> FsResult; fn lookup(&self, path: &[u8]) -> FsResult; fn lookup_parent(&self, path: &[u8]) -> FsResult; } impl TreeExt for Tree { fn lookup_segs(&self, segs: Vec<&[u8]>) -> FsResult { let mut node_id = tree::ROOT_ID; let mut is_dir = true; for seg in segs.into_iter() { if !is_dir { return Err(FsError::Forbidden); } if self.get_node(node_id)?.is_dir() { node_id = self.get_child(node_id, seg)?; } else { is_dir = false; } } Ok(node_id) } fn lookup(&self, path: &[u8]) -> FsResult { self.lookup_segs( path.split(|&c| c == b'/') .filter(|s| !s.is_empty()) .collect(), ) } // pop the last segment off the path, do a lookup, then // check if the result is a directory. fn lookup_parent(&self, path: &[u8]) -> FsResult { let mut segs: Vec<&[u8]> = path .split(|&c| c == b'/') .filter(|s| !s.is_empty()) .collect(); segs.pop(); let node_id = self.lookup_segs(segs)?; if !self.get_node(node_id)?.is_dir() { return Err(FsError::Forbidden); } Ok(node_id) } } // helper fn file_name(path: &[u8]) -> Vec { path.split(|&c| c == b'/') .rfind(|s| !s.is_empty()) .unwrap_or(b"") .to_vec() } dav-server-0.11.0/src/memls.rs000064400000000000000000000270561046102023000142450ustar 00000000000000//! Simple in-memory locksystem. //! //! This implementation has state - if you create a //! new instance in a handler(), it will be empty every time. //! //! This means you have to create the instance once, using `MemLs::new`, store //! it in your handler struct, and clone() it every time you pass //! it to the DavHandler. As a MemLs struct is just a handle, cloning is cheap. use std::collections::HashMap; use std::future; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; use futures_util::FutureExt; use uuid::Uuid; use xmltree::Element; use crate::davpath::DavPath; use crate::fs::FsResult; use crate::ls::*; use crate::tree; type Tree = tree::Tree, Vec>; /// Ephemeral in-memory LockSystem. #[derive(Debug, Clone)] pub struct MemLs(Arc>); #[derive(Debug)] struct MemLsInner { tree: Tree, #[allow(dead_code)] locks: HashMap, u64>, } impl MemLs { /// Create a new "memls" locksystem. pub fn new() -> Box { let inner = MemLsInner { tree: Tree::new(Vec::new()), locks: HashMap::new(), }; Box::new(MemLs(Arc::new(Mutex::new(inner)))) } } impl DavLockSystem for MemLs { fn lock( &'_ self, path: &DavPath, principal: Option<&str>, owner: Option<&Element>, timeout: Option, shared: bool, deep: bool, ) -> LsFuture<'_, Result> { let inner = &mut *self.0.lock().unwrap(); // any locks in the path? let rc = check_locks_to_path(&inner.tree, path, None, true, &Vec::new(), shared); trace!("lock: check_locks_to_path: {rc:?}"); if let Err(err) = rc { return future::ready(Err(err)).boxed(); } // if it's a deep lock we need to check if there are locks furter along the path. if deep { let rc = check_locks_from_path(&inner.tree, path, None, true, &Vec::new(), shared); trace!("lock: check_locks_from_path: {rc:?}"); if let Err(err) = rc { return future::ready(Err(err)).boxed(); } } // create lock. let node = get_or_create_path_node(&mut inner.tree, path); let timeout_at = timeout.map(|d| SystemTime::now() + d); let lock = DavLock { token: Uuid::new_v4().urn().to_string(), path: Box::new(path.clone()), principal: principal.map(|s| s.to_string()), owner: owner.map(|o| Box::new(o.clone())), timeout_at, timeout, shared, deep, }; trace!("lock {} created", &lock.token); let slock = lock.clone(); node.push(slock); future::ready(Ok(lock)).boxed() } fn unlock(&'_ self, path: &DavPath, token: &str) -> LsFuture<'_, Result<(), ()>> { let inner = &mut *self.0.lock().unwrap(); let node_id = match lookup_lock(&inner.tree, path, token) { None => { trace!("unlock: {token} not found at {path}"); return future::ready(Err(())).boxed(); } Some(n) => n, }; let len = { let node = inner.tree.get_node_mut(node_id).unwrap(); let idx = node.iter().position(|n| n.token.as_str() == token).unwrap(); node.remove(idx); node.len() }; if len == 0 { inner.tree.delete_node(node_id).ok(); } future::ready(Ok(())).boxed() } fn refresh( &'_ self, path: &DavPath, token: &str, timeout: Option, ) -> LsFuture<'_, Result> { trace!("refresh lock {token}"); let inner = &mut *self.0.lock().unwrap(); let node_id = match lookup_lock(&inner.tree, path, token) { None => { trace!("lock not found"); return future::ready(Err(())).boxed(); } Some(n) => n, }; let node = inner.tree.get_node_mut(node_id).unwrap(); let idx = node.iter().position(|n| n.token.as_str() == token).unwrap(); let lock = &mut node[idx]; let timeout_at = timeout.map(|d| SystemTime::now() + d); lock.timeout = timeout; lock.timeout_at = timeout_at; future::ready(Ok(lock.clone())).boxed() } fn check( &'_ self, path: &DavPath, principal: Option<&str>, ignore_principal: bool, deep: bool, submitted_tokens: &[String], ) -> LsFuture<'_, Result<(), DavLock>> { let inner = &*self.0.lock().unwrap(); let _st = submitted_tokens; let rc = check_locks_to_path( &inner.tree, path, principal, ignore_principal, submitted_tokens, false, ); trace!("check: check_lock_to_path: {_st:?}: {rc:?}"); if let Err(err) = rc { return future::ready(Err(err)).boxed(); } // if it's a deep lock we need to check if there are locks furter along the path. if deep { let rc = check_locks_from_path( &inner.tree, path, principal, ignore_principal, submitted_tokens, false, ); trace!("check: check_locks_from_path: {rc:?}"); if let Err(err) = rc { return future::ready(Err(err)).boxed(); } } future::ready(Ok(())).boxed() } fn discover(&'_ self, path: &DavPath) -> LsFuture<'_, Vec> { let inner = &*self.0.lock().unwrap(); future::ready(list_locks(&inner.tree, path)).boxed() } fn delete(&'_ self, path: &DavPath) -> LsFuture<'_, Result<(), ()>> { let inner = &mut *self.0.lock().unwrap(); if let Some(node_id) = lookup_node(&inner.tree, path) { inner.tree.delete_subtree(node_id).ok(); } future::ready(Ok(())).boxed() } } // check if there are any locks along the path. fn check_locks_to_path( tree: &Tree, path: &DavPath, principal: Option<&str>, ignore_principal: bool, submitted_tokens: &[String], shared_ok: bool, ) -> Result<(), DavLock> { // path segments let segs = path_to_segs(path, true); let last_seg = segs.len() - 1; // state let mut holds_lock = false; let mut first_lock_seen: Option<&DavLock> = None; // walk over path segments starting at root. let mut node_id = tree::ROOT_ID; for (i, seg) in segs.into_iter().enumerate() { node_id = match get_child(tree, node_id, seg) { Ok(n) => n, Err(_) => break, }; let node_locks = match tree.get_node(node_id) { Ok(n) => n, Err(_) => break, }; for nl in node_locks { if i < last_seg && !nl.deep { continue; } if submitted_tokens.iter().any(|t| &nl.token == t) && (ignore_principal || principal == nl.principal.as_deref()) { // fine, we hold this lock. holds_lock = true; } else { // exclusive locks are fatal. if !nl.shared { return Err(nl.to_owned()); } // remember first shared lock seen. if !shared_ok { first_lock_seen.get_or_insert(nl); } } } } // return conflicting lock on error. if !holds_lock && let Some(first_lock_seen) = first_lock_seen { return Err(first_lock_seen.to_owned()); } Ok(()) } // See if there are locks in any path below this collection. fn check_locks_from_path( tree: &Tree, path: &DavPath, principal: Option<&str>, ignore_principal: bool, submitted_tokens: &[String], shared_ok: bool, ) -> Result<(), DavLock> { let node_id = match lookup_node(tree, path) { Some(id) => id, None => return Ok(()), }; check_locks_from_node( tree, node_id, principal, ignore_principal, submitted_tokens, shared_ok, ) } // See if there are locks in any nodes below this node. fn check_locks_from_node( tree: &Tree, node_id: u64, principal: Option<&str>, ignore_principal: bool, submitted_tokens: &[String], shared_ok: bool, ) -> Result<(), DavLock> { let node_locks = match tree.get_node(node_id) { Ok(n) => n, Err(_) => return Ok(()), }; for nl in node_locks { if (!nl.shared || !shared_ok) && (!submitted_tokens.iter().any(|t| t == &nl.token) || (!ignore_principal && principal != nl.principal.as_deref())) { return Err(nl.to_owned()); } } if let Ok(children) = tree.get_children(node_id) { for (_, node_id) in children { check_locks_from_node( tree, node_id, principal, ignore_principal, submitted_tokens, shared_ok, )?; } } Ok(()) } // Find or create node. fn get_or_create_path_node<'a>(tree: &'a mut Tree, path: &DavPath) -> &'a mut Vec { let mut node_id = tree::ROOT_ID; for seg in path_to_segs(path, false) { node_id = match tree.get_child(node_id, seg) { Ok(n) => n, Err(_) => tree .add_child(node_id, seg.to_vec(), Vec::new(), false) .unwrap(), }; } tree.get_node_mut(node_id).unwrap() } // Find lock in path. fn lookup_lock(tree: &Tree, path: &DavPath, token: &str) -> Option { trace!("lookup_lock: {token}"); let mut node_id = tree::ROOT_ID; for seg in path_to_segs(path, true) { trace!( "lookup_lock: node {} seg {}", node_id, String::from_utf8_lossy(seg) ); node_id = match get_child(tree, node_id, seg) { Ok(n) => n, Err(_) => break, }; let node = tree.get_node(node_id).unwrap(); trace!("lookup_lock: locks here: {:?}", &node); if node.iter().any(|n| n.token == token) { return Some(node_id); } } trace!("lookup_lock: fail"); None } // Find node ID for path. fn lookup_node(tree: &Tree, path: &DavPath) -> Option { let mut node_id = tree::ROOT_ID; for seg in path_to_segs(path, false) { node_id = match tree.get_child(node_id, seg) { Ok(n) => n, Err(_) => return None, }; } Some(node_id) } // Find all locks in a path fn list_locks(tree: &Tree, path: &DavPath) -> Vec { let mut locks = Vec::new(); let mut node_id = tree::ROOT_ID; if let Ok(node) = tree.get_node(node_id) { locks.extend_from_slice(node); } for seg in path_to_segs(path, false) { node_id = match tree.get_child(node_id, seg) { Ok(n) => n, Err(_) => break, }; if let Ok(node) = tree.get_node(node_id) { locks.extend_from_slice(node); } } locks } fn path_to_segs(path: &DavPath, include_root: bool) -> Vec<&[u8]> { let path = path.as_bytes(); let mut segs: Vec<&[u8]> = path .split(|&c| c == b'/') .filter(|s| !s.is_empty()) .collect(); if include_root { segs.insert(0, b""); } segs } fn get_child(tree: &Tree, node_id: u64, seg: &[u8]) -> FsResult { if seg.is_empty() { return Ok(node_id); } tree.get_child(node_id, seg) } dav-server-0.11.0/src/multierror.rs000064400000000000000000000113151046102023000153230ustar 00000000000000use std::io; use futures_util::{Stream, StreamExt}; use http::{Response, StatusCode}; use xml::EmitterConfig; use xml::common::XmlVersion; use xml::writer::EventWriter; use xml::writer::XmlEvent as XmlWEvent; use crate::DavError; use crate::async_stream::AsyncStream; use crate::body::Body; use crate::davpath::DavPath; use crate::util::MemBuffer; type Sender = crate::async_stream::Sender<(DavPath, StatusCode), DavError>; pub(crate) struct MultiError(Sender); impl MultiError { pub fn new(sender: Sender) -> MultiError { MultiError(sender) } pub async fn add_status<'a>( &'a mut self, path: &'a DavPath, status: impl Into + 'static, ) -> Result<(), futures_channel::mpsc::SendError> { let status = status.into().statuscode(); self.0.send((path.clone(), status)).await; Ok(()) } } type XmlWriter<'a> = EventWriter; fn write_elem<'b, S>(xw: &mut XmlWriter, name: S, text: &str) -> Result<(), DavError> where S: Into>, { let n = name.into(); xw.write(XmlWEvent::start_element(n))?; if !text.is_empty() { xw.write(XmlWEvent::characters(text))?; } xw.write(XmlWEvent::end_element())?; Ok(()) } fn write_response(w: &mut XmlWriter, path: &DavPath, sc: StatusCode) -> Result<(), DavError> { w.write(XmlWEvent::start_element("D:response"))?; let p = path.with_prefix().as_url_string(); write_elem(w, "D:href", &p)?; write_elem(w, "D:status", &format!("HTTP/1.1 {sc}"))?; w.write(XmlWEvent::end_element())?; Ok(()) } pub(crate) async fn multi_error( req_path: DavPath, status_stream: S, ) -> Result, DavError> where S: Stream> + Send + 'static, { // read the first path/status item let mut status_stream = Box::pin(status_stream); let (path, status) = match status_stream.next().await { None => { debug!("multi_error: empty status_stream"); return Err(DavError::ChanError); } Some(Err(e)) => return Err(e), Some(Ok(item)) => item, }; let mut items = Vec::new(); if path == req_path { // the first path/status item was for the request path. // see if there is a next item. match status_stream.next().await { None => { // No, this was the first and only item. let resp = Response::builder() .status(status) .body(Body::empty()) .unwrap(); return Ok(resp); } Some(Err(e)) => return Err(e), Some(Ok(item)) => { // Yes, more than one response. items.push(Ok((path, status))); items.push(Ok(item)); } } } else { items.push(Ok((path, status))); } // Transform path/status items to XML. let body = AsyncStream::new(|mut tx| { async move { // Write initial header. let mut xw = EventWriter::new_with_config( MemBuffer::new(), EmitterConfig { perform_indent: true, ..EmitterConfig::default() }, ); xw.write(XmlWEvent::StartDocument { version: XmlVersion::Version10, encoding: Some("utf-8"), standalone: None, }) .map_err(DavError::from)?; xw.write(XmlWEvent::start_element("D:multistatus").ns("D", "DAV:")) .map_err(DavError::from)?; let data = xw.inner_mut().take(); tx.send(data).await; // now write the items. let mut status_stream = futures_util::stream::iter(items).chain(status_stream); while let Some(res) = status_stream.next().await { let (path, status) = res?; let status = if status == StatusCode::NO_CONTENT { StatusCode::OK } else { status }; write_response(&mut xw, &path, status)?; let data = xw.inner_mut().take(); tx.send(data).await; } // and finally write the trailer. xw.write(XmlWEvent::end_element()).map_err(DavError::from)?; let data = xw.inner_mut().take(); tx.send(data).await; Ok::<_, io::Error>(()) } }); // return response. let resp = Response::builder() .header("content-type", "application/xml; charset=utf-8") .status(StatusCode::MULTI_STATUS) .body(Body::from(body)) .unwrap(); Ok(resp) } dav-server-0.11.0/src/tree.rs000064400000000000000000000141721046102023000140620ustar 00000000000000use std::borrow::Borrow; use std::collections::HashMap; use std::fmt::Debug; use std::hash::Hash; use crate::FsError; use crate::FsResult; #[derive(Debug)] /// A tree contains a bunch of nodes. pub struct Tree { nodes: HashMap>, node_id: u64, } /// id of the root node of the tree. pub const ROOT_ID: u64 = 1; #[derive(Debug)] /// Node itself. "data" contains user-modifiable data. pub struct Node { pub data: D, #[allow(dead_code)] id: u64, parent_id: u64, children: HashMap, } #[derive(Debug)] // Iterator over the children of a node. pub struct Children(std::vec::IntoIter<(K, u64)>); impl Tree { /// Get new tree and initialize the root with 'data'. pub fn new(data: D) -> Tree { let mut t = Tree { nodes: HashMap::new(), node_id: ROOT_ID, }; t.new_node(99999999, data); t } fn new_node(&mut self, parent: u64, data: D) -> u64 { let id = self.node_id; self.node_id += 1; let node = Node { id, parent_id: parent, data, children: HashMap::new(), }; self.nodes.insert(id, node); id } /// add a child node to an existing node. pub fn add_child(&mut self, parent: u64, key: K, data: D, overwrite: bool) -> FsResult { { let pnode = self.nodes.get(&parent).ok_or(FsError::NotFound)?; if !overwrite && pnode.children.contains_key(&key) { return Err(FsError::Exists); } } let id = self.new_node(parent, data); let pnode = self.nodes.get_mut(&parent).unwrap(); pnode.children.insert(key, id); Ok(id) } /* * unused ... pub fn remove_child(&mut self, parent: u64, key: &K) -> FsResult<()> { let id = { let pnode = self.nodes.get(&parent).ok_or(FsError::NotFound)?; let id = *pnode.children.get(key).ok_or(FsError::NotFound)?; let node = self.nodes.get(&id).unwrap(); if node.children.len() > 0 { return Err(FsError::Forbidden); } id }; { let pnode = self.nodes.get_mut(&parent).unwrap(); pnode.children.remove(key); } self.nodes.remove(&id); Ok(()) }*/ /// Get a child node by key K. pub fn get_child(&self, parent: u64, key: &Q) -> FsResult where K: Borrow, Q: Hash + Eq + ?Sized, { let pnode = self.nodes.get(&parent).ok_or(FsError::NotFound)?; let id = pnode.children.get(key).ok_or(FsError::NotFound)?; Ok(*id) } /// Get all children of this node. Returns an iterator over . pub fn get_children(&self, parent: u64) -> FsResult> { let pnode = self.nodes.get(&parent).ok_or(FsError::NotFound)?; let mut v = Vec::new(); for (k, i) in &pnode.children { v.push(((*k).clone(), *i)); } Ok(Children(v.into_iter())) } /// Get reference to a node. pub fn get_node(&self, id: u64) -> FsResult<&D> { let n = self.nodes.get(&id).ok_or(FsError::NotFound)?; Ok(&n.data) } /// Get mutable reference to a node. pub fn get_node_mut(&mut self, id: u64) -> FsResult<&mut D> { let n = self.nodes.get_mut(&id).ok_or(FsError::NotFound)?; Ok(&mut n.data) } fn delete_node_from_parent(&mut self, id: u64) -> FsResult<()> { let parent_id = self.nodes.get(&id).ok_or(FsError::NotFound)?.parent_id; let key = { let pnode = self.nodes.get(&parent_id).unwrap(); let mut key = None; for (k, i) in &pnode.children { if i == &id { key = Some((*k).clone()); break; } } key }; let key = key.unwrap(); let pnode = self.nodes.get_mut(&parent_id).unwrap(); pnode.children.remove(&key); Ok(()) } /// Delete a node. Fails if node has children. Returns node itself. pub fn delete_node(&mut self, id: u64) -> FsResult> { { let n = self.nodes.get(&id).ok_or(FsError::NotFound)?; if !n.children.is_empty() { return Err(FsError::Forbidden); } } self.delete_node_from_parent(id)?; Ok(self.nodes.remove(&id).unwrap()) } /// Delete a subtree. pub fn delete_subtree(&mut self, id: u64) -> FsResult<()> { let children = { let n = self.nodes.get(&id).ok_or(FsError::NotFound)?; n.children.iter().map(|(_, &v)| v).collect::>() }; for c in children.into_iter() { self.delete_subtree(c)?; } self.delete_node_from_parent(id) } /// Move a node to a new position and new name in the tree. /// If "overwrite" is true, will replace an existing /// node, but only if it doesn't have any children. #[cfg(feature = "memfs")] pub fn move_node( &mut self, id: u64, new_parent: u64, new_name: K, overwrite: bool, ) -> FsResult<()> { let dest = { let pnode = self.nodes.get(&new_parent).ok_or(FsError::NotFound)?; if let Some(cid) = pnode.children.get(&new_name) { let cnode = self.nodes.get(cid).unwrap(); if !overwrite || !cnode.children.is_empty() { return Err(FsError::Exists); } Some(*cid) } else { None } }; self.delete_node_from_parent(id)?; self.nodes.get_mut(&id).unwrap().parent_id = new_parent; if let Some(dest) = dest { self.nodes.remove(&dest); } let pnode = self.nodes.get_mut(&new_parent).unwrap(); pnode.children.insert(new_name, id); Ok(()) } } impl Iterator for Children { type Item = (K, u64); fn next(&mut self) -> Option { self.0.next() } } dav-server-0.11.0/src/util.rs000064400000000000000000000144001046102023000140720ustar 00000000000000use std::io::{Cursor, Write}; use std::time::SystemTime; use bytes::Bytes; use chrono::{DateTime, SecondsFormat, Utc}; use headers::Header; use http::method::InvalidMethod; use crate::DavResult; use crate::body::Body; use crate::errors::DavError; /// HTTP Methods supported by DavHandler. #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] #[repr(u32)] pub enum DavMethod { Head = 0x0001, Get = 0x0002, Put = 0x0004, Patch = 0x0008, Options = 0x0010, PropFind = 0x0020, PropPatch = 0x0040, MkCol = 0x0080, Copy = 0x0100, Move = 0x0200, Delete = 0x0400, Lock = 0x0800, Unlock = 0x1000, Report = 0x2000, MkCalendar = 0x4000, MkAddressbook = 0x8000, } // translate method into our own enum that has webdav methods as well. pub(crate) fn dav_method(m: &http::Method) -> DavResult { let m = match *m { http::Method::HEAD => DavMethod::Head, http::Method::GET => DavMethod::Get, http::Method::PUT => DavMethod::Put, http::Method::PATCH => DavMethod::Patch, http::Method::DELETE => DavMethod::Delete, http::Method::OPTIONS => DavMethod::Options, _ => match m.as_str() { "PROPFIND" => DavMethod::PropFind, "PROPPATCH" => DavMethod::PropPatch, "MKCOL" => DavMethod::MkCol, "COPY" => DavMethod::Copy, "MOVE" => DavMethod::Move, "LOCK" => DavMethod::Lock, "UNLOCK" => DavMethod::Unlock, "REPORT" => DavMethod::Report, "MKCALENDAR" => DavMethod::MkCalendar, "MKADDRESSBOOK" => DavMethod::MkAddressbook, _ => { return Err(DavError::UnknownDavMethod); } }, }; Ok(m) } // for external use. impl std::convert::TryFrom<&http::Method> for DavMethod { type Error = InvalidMethod; fn try_from(value: &http::Method) -> Result { dav_method(value).map_err(|_| { // A trick to get at the value of http::method::InvalidMethod. http::method::Method::from_bytes(b"").unwrap_err() }) } } /// A set of allowed [`DavMethod`]s. /// /// [`DavMethod`]: enum.DavMethod.html #[derive(Clone, Copy, Debug)] pub struct DavMethodSet(u32); impl DavMethodSet { pub const HTTP_RO: DavMethodSet = DavMethodSet(DavMethod::Get as u32 | DavMethod::Head as u32 | DavMethod::Options as u32); pub const HTTP_RW: DavMethodSet = DavMethodSet(Self::HTTP_RO.0 | DavMethod::Put as u32); pub const WEBDAV_RO: DavMethodSet = DavMethodSet(Self::HTTP_RO.0 | DavMethod::PropFind as u32); pub const WEBDAV_RW: DavMethodSet = DavMethodSet(0xffffffff); /// New set, all methods allowed. pub fn all() -> DavMethodSet { DavMethodSet(0xffffffff) } /// New empty set. pub fn none() -> DavMethodSet { DavMethodSet(0) } /// Add a method. pub fn add(&mut self, m: DavMethod) -> &Self { self.0 |= m as u32; self } /// Remove a method. pub fn remove(&mut self, m: DavMethod) -> &Self { self.0 &= !(m as u32); self } /// Check if a method is in the set. pub fn contains(&self, m: DavMethod) -> bool { self.0 & (m as u32) > 0 } /// Generate an DavMethodSet from a list of words. pub fn from_vec(v: Vec>) -> Result { let mut m: u32 = 0; for w in &v { m |= match w.as_ref().to_lowercase().as_str() { "head" => DavMethod::Head as u32, "get" => DavMethod::Get as u32, "put" => DavMethod::Put as u32, "patch" => DavMethod::Patch as u32, "delete" => DavMethod::Delete as u32, "options" => DavMethod::Options as u32, "propfind" => DavMethod::PropFind as u32, "proppatch" => DavMethod::PropPatch as u32, "mkcol" => DavMethod::MkCol as u32, "copy" => DavMethod::Copy as u32, "move" => DavMethod::Move as u32, "lock" => DavMethod::Lock as u32, "unlock" => DavMethod::Unlock as u32, "report" => DavMethod::Report as u32, "mkcalendar" => DavMethod::MkCalendar as u32, "mkaddressbook" => DavMethod::MkAddressbook as u32, "http-ro" => Self::HTTP_RO.0, "http-rw" => Self::HTTP_RW.0, "webdav-ro" => Self::WEBDAV_RO.0, "webdav-rw" => Self::WEBDAV_RW.0, _ => { // A trick to get at the value of http::method::InvalidMethod. let invalid_method = http::method::Method::from_bytes(b"").unwrap_err(); return Err(invalid_method); } }; } Ok(DavMethodSet(m)) } } pub(crate) fn dav_xml_error(body: &str) -> Body { let xml = format!( "{}\n{}\n{}\n{}\n", r#""#, r#""#, body, r#""# ); Body::from(xml) } pub(crate) fn systemtime_to_httpdate(t: SystemTime) -> String { let d = headers::Date::from(t); let mut v = Vec::new(); d.encode(&mut v); v[0].to_str().unwrap().to_owned() } pub(crate) fn systemtime_to_rfc3339_without_nanosecond(t: SystemTime) -> String { // 1996-12-19T16:39:57Z DateTime::::from(t).to_rfc3339_opts(SecondsFormat::Secs, true) } // A buffer that implements "Write". #[derive(Clone)] pub(crate) struct MemBuffer(Cursor>); impl MemBuffer { pub fn new() -> MemBuffer { MemBuffer(Cursor::new(Vec::new())) } pub fn take(&mut self) -> Bytes { let buf = std::mem::take(self.0.get_mut()); self.0.set_position(0); Bytes::from(buf) } } impl Write for MemBuffer { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.0.write(buf) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } #[cfg(test)] mod tests { use super::*; use std::time::UNIX_EPOCH; #[test] fn test_rfc3339_no_nanosecond() { let t = UNIX_EPOCH + std::time::Duration::new(1, 5); assert!(systemtime_to_rfc3339_without_nanosecond(t) == "1970-01-01T00:00:01Z"); } } dav-server-0.11.0/src/voidfs.rs000064400000000000000000000030741046102023000144140ustar 00000000000000//! Placeholder filesystem. Returns FsError::NotImplemented on every method. //! use std::{any::Any, marker::PhantomData}; use crate::davpath::DavPath; use crate::fs::*; /// Placeholder filesystem. #[derive(Clone, Debug)] pub struct VoidFs { _marker: PhantomData, } pub fn is_voidfs(fs: &dyn Any) -> bool { fs.is::>>() } impl VoidFs { pub fn new() -> Box { Box::new(Self { _marker: Default::default(), }) } } impl GuardedFileSystem for VoidFs { fn metadata<'a>( &'a self, _path: &'a DavPath, _credentials: &C, ) -> FsFuture<'a, Box> { Box::pin(async { Err(FsError::NotImplemented) }) } fn read_dir<'a>( &'a self, _path: &'a DavPath, _meta: ReadDirMeta, _credentials: &C, ) -> FsFuture<'a, FsStream>> { Box::pin(async { Err(FsError::NotImplemented) }) } fn open<'a>( &'a self, _path: &'a DavPath, _options: OpenOptions, _credentials: &C, ) -> FsFuture<'a, Box> { Box::pin(async { Err(FsError::NotImplemented) }) } } #[cfg(test)] mod tests { use super::*; use crate::memfs::MemFs; #[test] fn test_is_void() { assert!(is_voidfs::(&VoidFs::::new())); assert!(is_voidfs::<()>(&VoidFs::<()>::new())); assert!(!is_voidfs::<()>(&MemFs::new())); } } dav-server-0.11.0/src/warp.rs000064400000000000000000000145231046102023000140740ustar 00000000000000//! Adapter for the `warp` HTTP server framework. //! //! The filters in this module will always succeed and never //! return an error. For example, if a file is not found, the //! filter will return a 404 reply, and not an internal //! rejection. //! use std::convert::Infallible; #[cfg(any(docsrs, feature = "localfs"))] use std::path::Path; use warp::{ Filter, Reply, filters::BoxedFilter, http::{HeaderMap, Method}, }; use crate::{DavHandler, body::Body}; #[cfg(any(docsrs, feature = "localfs"))] use crate::{fakels::FakeLs, localfs::LocalFs}; /// Reply-filter that runs a DavHandler. /// /// Just pass in a pre-configured DavHandler. If a prefix was not /// configured, it will be the request path up to this point. pub fn dav_handler(handler: DavHandler) -> BoxedFilter<(impl Reply,)> { use http::uri::Uri; use warp::path::{FullPath, Tail}; warp::method() .and(warp::path::full()) .and(warp::path::tail()) .and(warp::header::headers_cloned()) .and(warp::body::stream()) .and_then( move |method: Method, path_full: FullPath, path_tail: Tail, headers: HeaderMap, body| { let handler = handler.clone(); async move { // rebuild an http::Request struct. let path_str = path_full.as_str(); let uri = path_str.parse::().unwrap(); let mut builder = http::Request::builder().method(method.as_ref()).uri(uri); for (k, v) in headers.iter() { builder = builder.header(k.as_str(), v.as_ref()); } let request = builder.body(body).unwrap(); let response = if handler.config.prefix.is_some() { // Run a handler with the configured path prefix. handler.handle_stream(request).await } else { // Run a handler with the current path prefix. let path_len = path_str.len(); let tail_len = path_tail.as_str().len(); let prefix = path_str[..path_len - tail_len].to_string(); let config = DavHandler::builder().strip_prefix(prefix); handler.handle_stream_with(config, request).await }; // Need to remap the http_body::Body to a hyper::Body. let response = warp_response(response).unwrap(); Ok::<_, Infallible>(response) } }, ) .boxed() } /// Creates a Filter that serves files and directories at the /// base path joined with the remainder of the request path, /// like `warp::filters::fs::dir`. /// /// The behaviour for serving a directory depends on the flags: /// /// - `index_html`: if an `index.html` file is found, serve it. /// - `auto_index_over_get`: Create a directory index page when accessing over HTTP `GET` (but NOT /// affecting WebDAV `PROPFIND` method currently). In the current implementation, this only /// affects HTTP `GET` method (commonly used for listing the directories when accessing through a /// `http://` or `https://` URL for a directory in a browser), but NOT WebDAV listing of a /// directory (HTTP `PROPFIND`). BEWARE: The name and behaviour of this parameter variable may /// change, and later it may control WebDAV `PROPFIND`, too (but not as of now). /// /// In release mode, if `auto_index_over_get` is `true`, then this executes as described above /// (currently affecting only HTTP `GET`), but beware of this current behaviour. /// /// In debug mode, if `auto_index_over_get` is `false`, this _panics_. That is so that it alerts /// the developers to this current limitation, so they don't accidentally expect /// `auto_index_over_get` to control WebDAV. /// - no flags set: 404. #[cfg(any(docsrs, feature = "localfs"))] pub fn dav_dir( base: impl AsRef, index_html: bool, auto_index_over_get: bool, ) -> BoxedFilter<(impl Reply,)> { debug_assert!( auto_index_over_get, "See documentation of dav_server::warp::dav_dir(...)." ); let mut builder = DavHandler::builder() .filesystem(LocalFs::new(base, false, false, false)) .locksystem(FakeLs::new()) .autoindex(auto_index_over_get); if index_html { builder = builder.indexfile("index.html".to_string()) } let handler = builder.build_handler(); dav_handler(handler) } /// Creates a Filter that serves a single file, ignoring the request path, /// like `warp::filters::fs::file`. #[cfg(any(docsrs, feature = "localfs"))] pub fn dav_file(file: impl AsRef) -> BoxedFilter<(impl Reply,)> { let handler = DavHandler::builder() .filesystem(LocalFs::new_file(file, false)) .locksystem(FakeLs::new()) .build_handler(); dav_handler(handler) } /// Adapts the response to the `warp` versions of `hyper` and `http` while `warp` remains on old versions. /// https://github.com/seanmonstar/warp/issues/1088 fn warp_response( response: http::Response, ) -> Result, warp::http::Error> { let (parts, body) = response.into_parts(); // Leave response extensions empty. let mut response = warp::http::Response::builder() .version(warp_http_version(parts.version)) .status(parts.status.as_u16()); // Ignore headers without the name. let headers = parts.headers.into_iter().filter_map(|(k, v)| Some((k?, v))); for (k, v) in headers { response = response.header(k.as_str(), v.as_ref()); } response.body(warp::hyper::Body::wrap_stream(body)) } /// Adapts HTTP version to the `warp` version of `http` crate while `warp` remains on old version. /// https://github.com/seanmonstar/warp/issues/1088 fn warp_http_version(v: http::Version) -> warp::http::Version { match v { http::Version::HTTP_3 => warp::http::Version::HTTP_3, http::Version::HTTP_2 => warp::http::Version::HTTP_2, http::Version::HTTP_11 => warp::http::Version::HTTP_11, http::Version::HTTP_10 => warp::http::Version::HTTP_10, http::Version::HTTP_09 => warp::http::Version::HTTP_09, v => unreachable!("unexpected HTTP version {:?}", v), } } dav-server-0.11.0/src/xmltree_ext.rs000064400000000000000000000133471046102023000154660ustar 00000000000000use std::borrow::Cow; use std::io::{Read, Write}; use xml::EmitterConfig; use xml::common::XmlVersion; use xml::writer::EventWriter; use xml::writer::XmlEvent as XmlWEvent; use xmltree::{self, Element, XMLNode}; use crate::{DavError, DavResult}; pub(crate) trait ElementExt { /// Builder. fn new2<'a, E: Into<&'a str>>(e: E) -> Self; /// Builder. fn ns>(self, prefix: S, namespace: S) -> Self; /// Builder. fn text>(self, t: T) -> Self; /// Like parse, but returns DavError. fn parse2(r: R) -> Result; /// Add a child element. fn push_element(&mut self, e: Element); /// Iterator over the children that are Elements. fn child_elems_into_iter(self) -> Box>; /// Iterator over the children that are Elements. fn child_elems_iter<'a>(&'a self) -> Box + 'a>; /// Vec of the children that are Elements. fn take_child_elems(self) -> Vec; /// Does the element have children that are also Elements. fn has_child_elems(&self) -> bool; /// Write the element using an EventWriter. fn write_ev(&self, emitter: &mut EventWriter) -> xml::writer::Result<()>; } impl ElementExt for Element { fn ns>(mut self, prefix: S, namespace: S) -> Element { let mut ns = self.namespaces.unwrap_or_else(xmltree::Namespace::empty); ns.force_put(prefix.into(), namespace.into()); self.namespaces = Some(ns); self } fn new2<'a, N: Into<&'a str>>(n: N) -> Element { let v: Vec<&str> = n.into().splitn(2, ':').collect(); if v.len() == 1 { Element::new(v[0]) } else { let mut e = Element::new(v[1]); e.prefix = Some(v[0].to_string()); e } } fn text>(mut self, t: S) -> Element { let nodes = self .children .drain(..) .filter(|n| n.as_text().is_none()) .collect(); self.children = nodes; self.children.push(XMLNode::Text(t.into())); self } fn push_element(&mut self, e: Element) { self.children.push(XMLNode::Element(e)); } fn child_elems_into_iter(self) -> Box> { let iter = self.children.into_iter().filter_map(|n| match n { XMLNode::Element(e) => Some(e), _ => None, }); Box::new(iter) } fn child_elems_iter<'a>(&'a self) -> Box + 'a> { let iter = self.children.iter().filter_map(|n| n.as_element()); Box::new(iter) } fn take_child_elems(self) -> Vec { self.children .into_iter() .filter_map(|n| match n { XMLNode::Element(e) => Some(e), _ => None, }) .collect() } fn has_child_elems(&self) -> bool { self.children.iter().find_map(|n| n.as_element()).is_some() } fn parse2(r: R) -> Result { let res = Element::parse(r); match res { Ok(elems) => Ok(elems), Err(xmltree::ParseError::MalformedXml(_)) => Err(DavError::XmlParseError), Err(_) => Err(DavError::XmlReadError), } } fn write_ev(&self, emitter: &mut EventWriter) -> xml::writer::Result<()> { use xml::attribute::Attribute; use xml::name::Name; use xml::namespace::Namespace; use xml::writer::events::XmlEvent; let mut name = Name::local(&self.name); if let Some(ref ns) = self.namespace { name.namespace = Some(ns); } if let Some(ref p) = self.prefix { name.prefix = Some(p); } let mut attributes = Vec::with_capacity(self.attributes.len()); for (k, v) in &self.attributes { attributes.push(Attribute { name: Name::local(k), value: v, }); } let empty_ns = Namespace::empty(); let namespace = if let Some(ref ns) = self.namespaces { Cow::Borrowed(ns) } else { Cow::Borrowed(&empty_ns) }; emitter.write(XmlEvent::StartElement { name, attributes: Cow::Owned(attributes), namespace, })?; for node in &self.children { match node { XMLNode::Element(elem) => elem.write_ev(emitter)?, XMLNode::Text(text) => emitter.write(XmlEvent::Characters(text))?, XMLNode::Comment(comment) => emitter.write(XmlEvent::Comment(comment))?, XMLNode::CData(comment) => emitter.write(XmlEvent::CData(comment))?, XMLNode::ProcessingInstruction(name, data) => match data.to_owned() { Some(string) => emitter.write(XmlEvent::ProcessingInstruction { name, data: Some(&string), })?, None => emitter.write(XmlEvent::ProcessingInstruction { name, data: None })?, }, } // elem.write_ev(emitter)?; } emitter.write(XmlEvent::EndElement { name: Some(name) })?; Ok(()) } } pub(crate) fn emitter(w: W) -> DavResult> { let mut emitter = EventWriter::new_with_config( w, EmitterConfig { perform_indent: false, indent_string: Cow::Borrowed(""), ..Default::default() }, ); emitter.write(XmlWEvent::StartDocument { version: XmlVersion::Version10, encoding: Some("utf-8"), standalone: None, })?; Ok(emitter) } dav-server-0.11.0/tests/caldav_tests.rs000064400000000000000000000275111046102023000161530ustar 00000000000000#[cfg(feature = "caldav")] mod caldav_tests { use dav_server::{DavHandler, body::Body, caldav::*, fakels::FakeLs, memfs::MemFs}; use http::{Method, Request, StatusCode}; fn setup_caldav_server() -> DavHandler { DavHandler::builder() .filesystem(MemFs::new()) .locksystem(FakeLs::new()) .build_handler() } async fn resp_to_string(mut resp: http::Response) -> String { use futures_util::StreamExt; let mut data = Vec::new(); let body = resp.body_mut(); while let Some(chunk) = body.next().await { match chunk { Ok(bytes) => data.extend_from_slice(&bytes), Err(e) => panic!("Error reading body stream: {}", e), } } String::from_utf8(data).unwrap_or_else(|_| "".to_string()) } #[tokio::test] async fn test_caldav_options() { let server = setup_caldav_server(); let req = Request::builder() .method(Method::OPTIONS) .uri("/") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::OK); let dav_header = resp.headers().get("DAV").unwrap(); let dav_str = dav_header.to_str().unwrap(); assert!(dav_str.contains("calendar-access")); } #[tokio::test] async fn test_mkcalendar() { let server = setup_caldav_server(); let req = Request::builder() .method("MKCALENDAR") .uri("/calendars/my-calendar") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::CREATED); } #[tokio::test] async fn test_mkcalendar_already_exists() { let server = setup_caldav_server(); // First create a regular collection let req = Request::builder() .method("MKCOL") .uri("/calendars/my-calendar") .body(Body::empty()) .unwrap(); let _ = server.handle(req).await; // Try to create calendar collection on existing path let req = Request::builder() .method("MKCALENDAR") .uri("/calendars/my-calendar") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } #[tokio::test] async fn test_calendar_propfind() { let server = setup_caldav_server(); // Create a calendar collection first let req = Request::builder() .method("MKCALENDAR") .uri("/calendars/my-calendar") .body(Body::empty()) .unwrap(); let _ = server.handle(req).await; // PROPFIND request let propfind_body = r#" "#; let req = Request::builder() .method("PROPFIND") .uri("/calendars/my-calendar") .header("Depth", "0") .body(Body::from(propfind_body)) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::MULTI_STATUS); // Check that response contains CalDAV properties let body_str = resp_to_string(resp).await; assert!(body_str.contains("supported-calendar-component-set")); assert!(body_str.contains("supported-calendar-data")); } #[tokio::test] async fn test_calendar_event_put() { let server = setup_caldav_server(); // Create a calendar collection first let req = Request::builder() .method("MKCALENDAR") .uri("/calendars/my-calendar") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert!(resp.status().is_success()); // PUT a calendar event let ical_data = r#"BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:test-event-123@example.com DTSTART:20240101T120000Z DTEND:20240101T130000Z SUMMARY:Test Event DESCRIPTION:This is a test event END:VEVENT END:VCALENDAR"#; let req = Request::builder() .method(Method::PUT) .uri("/calendars/my-calendar/event.ics") .header("Content-Type", "text/calendar") .body(Body::from(ical_data)) .unwrap(); let resp = server.handle(req).await; assert!(resp.status().is_success()); } #[tokio::test] async fn test_calendar_query_report() { let server = setup_caldav_server(); // Create a calendar collection let req = Request::builder() .method("MKCALENDAR") .uri("/calendars/my-calendar") .body(Body::empty()) .unwrap(); let _ = server.handle(req).await; // Add a calendar event let ical_data = r#"BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:test-event-123@example.com DTSTART:20240101T120000Z DTEND:20240101T130000Z SUMMARY:Test Event END:VEVENT END:VCALENDAR"#; let req = Request::builder() .method(Method::PUT) .uri("/calendars/my-calendar/event.ics") .header("Content-Type", "text/calendar") .body(Body::from(ical_data)) .unwrap(); let _ = server.handle(req).await; // REPORT calendar-query let report_body = r#" "#; let req = Request::builder() .method("REPORT") .uri("/calendars/my-calendar") .header("Depth", "1") .body(Body::from(report_body)) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::MULTI_STATUS); let body_str = resp_to_string(resp).await; assert!(body_str.contains("calendar-data")); assert!(body_str.contains("Test Event")); } #[tokio::test] async fn test_calendar_multiget_report() { let server = setup_caldav_server(); // Create a calendar collection let req = Request::builder() .method("MKCALENDAR") .uri("/calendars/my-calendar") .body(Body::empty()) .unwrap(); let _ = server.handle(req).await; // Add a calendar event let ical_data = r#"BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:test-event-123@example.com DTSTART:20240101T120000Z DTEND:20240101T130000Z SUMMARY:Test Event0001 END:VEVENT END:VCALENDAR"#; let req = Request::builder() .method(Method::PUT) .uri("/calendars/my-calendar/event1.ics") .header("Content-Type", "text/calendar") .body(Body::from(ical_data)) .unwrap(); let _ = server.handle(req).await; // Add a calendar event let ical_data = r#"BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:test-event-123@example.com DTSTART:20250101T120000Z DTEND:20250101T130000Z SUMMARY:Test Event2222 END:VEVENT END:VCALENDAR"#; let req = Request::builder() .method(Method::PUT) .uri("/calendars/my-calendar/event2.ics") .header("Content-Type", "text/calendar") .body(Body::from(ical_data)) .unwrap(); let _ = server.handle(req).await; // REPORT calendar-multiget let report_body = r#" /calendars/my-calendar/event1.ics /calendars/my-calendar/event2.ics "#; let req = Request::builder() .method("REPORT") .uri("/calendars/my-calendar") .body(Body::from(report_body)) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::MULTI_STATUS); let body_str = resp_to_string(resp).await; assert!( body_str.contains("calendar-data"), "Response body missing 'calendar-data': {}", body_str ); assert!( body_str.contains("Test Event0001"), "Response body missing 'Test Event0001': {}", body_str ); assert!( body_str.contains("Test Event2222"), "Response body missing 'Test Event2222': {}", body_str ); } #[test] fn test_is_calendar_data() { let valid_ical = b"BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR\n"; assert!(is_calendar_data(valid_ical)); let invalid_data = b"This is not calendar data"; assert!(!is_calendar_data(invalid_data)); } #[test] fn test_extract_calendar_uid() { let ical_with_uid = "BEGIN:VCALENDAR\nUID:test-123@example.com\nEND:VCALENDAR"; assert_eq!( extract_calendar_uid(ical_with_uid), Some("test-123@example.com".to_string()) ); let ical_without_uid = "BEGIN:VCALENDAR\nSUMMARY:Test\nEND:VCALENDAR"; assert_eq!(extract_calendar_uid(ical_without_uid), None); } #[test] fn test_calendar_component_types() { assert_eq!(CalendarComponentType::VEvent.as_str(), "VEVENT"); assert_eq!(CalendarComponentType::VTodo.as_str(), "VTODO"); assert_eq!(CalendarComponentType::VJournal.as_str(), "VJOURNAL"); assert_eq!(CalendarComponentType::VFreeBusy.as_str(), "VFREEBUSY"); } #[test] fn test_calendar_properties_default() { let props = CalendarProperties::default(); assert!( props .supported_components .contains(&CalendarComponentType::VEvent) ); assert!( props .supported_components .contains(&CalendarComponentType::VTodo) ); assert_eq!(props.max_resource_size, Some(1024 * 1024)); } #[test] fn test_validate_calendar_data() { let valid_ical = r#"BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Test//Test//EN BEGIN:VEVENT UID:test@example.com DTSTART:20240101T120000Z DTEND:20240101T130000Z SUMMARY:Test END:VEVENT END:VCALENDAR"#; assert!(validate_calendar_data(valid_ical).is_ok()); } } #[cfg(not(feature = "caldav"))] mod caldav_disabled_tests { use dav_server::{DavHandler, body::Body, fakels::FakeLs, memfs::MemFs}; use http::Request; #[tokio::test] async fn test_caldav_methods_return_not_implemented() { let server = DavHandler::builder() .filesystem(MemFs::new()) .locksystem(FakeLs::new()) .build_handler(); // Test REPORT method - only returns NOT_IMPLEMENTED when carddav is also disabled #[cfg(not(feature = "carddav"))] { let req = Request::builder() .method("REPORT") .uri("/") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), http::StatusCode::NOT_IMPLEMENTED); } // Test MKCALENDAR method let req = Request::builder() .method("MKCALENDAR") .uri("/calendars/my-calendar") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), http::StatusCode::NOT_IMPLEMENTED); } } dav-server-0.11.0/tests/carddav_tests.rs000064400000000000000000000342561046102023000163310ustar 00000000000000#[cfg(feature = "carddav")] mod carddav_tests { use dav_server::{DavHandler, body::Body, carddav::*, fakels::FakeLs, memfs::MemFs}; use http::{Method, Request, StatusCode}; fn setup_carddav_server() -> DavHandler { DavHandler::builder() .filesystem(MemFs::new()) .locksystem(FakeLs::new()) .build_handler() } async fn resp_to_string(mut resp: http::Response) -> String { use futures_util::StreamExt; let mut data = Vec::new(); let body = resp.body_mut(); while let Some(chunk) = body.next().await { match chunk { Ok(bytes) => data.extend_from_slice(&bytes), Err(e) => panic!("Error reading body stream: {}", e), } } String::from_utf8(data).unwrap_or_else(|_| "".to_string()) } #[tokio::test] async fn test_carddav_options() { let server = setup_carddav_server(); let req = Request::builder() .method(Method::OPTIONS) .uri("/") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::OK); let dav_header = resp.headers().get("DAV").unwrap(); let dav_str = dav_header.to_str().unwrap(); assert!(dav_str.contains("addressbook")); } #[tokio::test] async fn test_mkaddressbook() { let server = setup_carddav_server(); let req = Request::builder() .method("MKADDRESSBOOK") .uri("/addressbooks/my-contacts") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::CREATED); } #[tokio::test] async fn test_mkaddressbook_already_exists() { let server = setup_carddav_server(); // First create a regular collection let req = Request::builder() .method("MKCOL") .uri("/addressbooks/my-contacts") .body(Body::empty()) .unwrap(); let _ = server.handle(req).await; // Try to create addressbook collection on existing path let req = Request::builder() .method("MKADDRESSBOOK") .uri("/addressbooks/my-contacts") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } #[tokio::test] async fn test_addressbook_propfind() { let server = setup_carddav_server(); // Create an addressbook collection first let req = Request::builder() .method("MKADDRESSBOOK") .uri("/addressbooks/my-contacts") .body(Body::empty()) .unwrap(); let _ = server.handle(req).await; // PROPFIND request let propfind_body = r#" "#; let req = Request::builder() .method("PROPFIND") .uri("/addressbooks/my-contacts") .header("Depth", "0") .body(Body::from(propfind_body)) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::MULTI_STATUS); // Check that response contains CardDAV properties let body_str = resp_to_string(resp).await; assert!(body_str.contains("supported-address-data")); } #[tokio::test] async fn test_addressbook_home_set() { let server = setup_carddav_server(); // PROPFIND request for addressbook-home-set on /addressbooks/ let propfind_body = r#" "#; let req = Request::builder() .method("PROPFIND") .uri("/addressbooks/") .header("Depth", "0") .body(Body::from(propfind_body)) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::MULTI_STATUS); // Check that response contains addressbook-home-set with correct href let body_str = resp_to_string(resp).await; assert!( body_str.contains("addressbook-home-set"), "Response body missing 'addressbook-home-set': {}", body_str ); assert!( body_str.contains("/addressbooks/"), "Response body missing '/addressbooks/' href: {}", body_str ); } #[tokio::test] async fn test_vcard_put() { let server = setup_carddav_server(); // Create an addressbook collection first let req = Request::builder() .method("MKADDRESSBOOK") .uri("/addressbooks/my-contacts") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert!(resp.status().is_success()); // PUT a vCard let vcard_data = r#"BEGIN:VCARD VERSION:3.0 UID:test-contact-123@example.com FN:John Doe N:Doe;John;;; EMAIL:john.doe@example.com TEL:+1-555-123-4567 END:VCARD"#; let req = Request::builder() .method(Method::PUT) .uri("/addressbooks/my-contacts/contact.vcf") .header("Content-Type", "text/vcard") .body(Body::from(vcard_data)) .unwrap(); let resp = server.handle(req).await; assert!(resp.status().is_success()); } #[tokio::test] async fn test_addressbook_query_report() { let server = setup_carddav_server(); // Create an addressbook collection let req = Request::builder() .method("MKADDRESSBOOK") .uri("/addressbooks/my-contacts") .body(Body::empty()) .unwrap(); let _ = server.handle(req).await; // Add a contact let vcard_data = r#"BEGIN:VCARD VERSION:3.0 UID:test-contact-123@example.com FN:John Doe N:Doe;John;;; EMAIL:john.doe@example.com END:VCARD"#; let req = Request::builder() .method(Method::PUT) .uri("/addressbooks/my-contacts/contact.vcf") .header("Content-Type", "text/vcard") .body(Body::from(vcard_data)) .unwrap(); let _ = server.handle(req).await; // REPORT addressbook-query let report_body = r#" "#; let req = Request::builder() .method("REPORT") .uri("/addressbooks/my-contacts") .header("Depth", "1") .body(Body::from(report_body)) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::MULTI_STATUS); let body_str = resp_to_string(resp).await; assert!(body_str.contains("address-data")); assert!(body_str.contains("John Doe")); } #[tokio::test] async fn test_addressbook_multiget_report() { let server = setup_carddav_server(); // Create an addressbook collection let req = Request::builder() .method("MKADDRESSBOOK") .uri("/addressbooks/my-contacts") .body(Body::empty()) .unwrap(); let _ = server.handle(req).await; // Add first contact let vcard_data1 = r#"BEGIN:VCARD VERSION:3.0 UID:contact-001@example.com FN:John Doe N:Doe;John;;; EMAIL:john.doe@example.com END:VCARD"#; let req = Request::builder() .method(Method::PUT) .uri("/addressbooks/my-contacts/contact1.vcf") .header("Content-Type", "text/vcard") .body(Body::from(vcard_data1)) .unwrap(); let _ = server.handle(req).await; // Add second contact let vcard_data2 = r#"BEGIN:VCARD VERSION:3.0 UID:contact-002@example.com FN:Jane Smith N:Smith;Jane;;; EMAIL:jane.smith@example.com END:VCARD"#; let req = Request::builder() .method(Method::PUT) .uri("/addressbooks/my-contacts/contact2.vcf") .header("Content-Type", "text/vcard") .body(Body::from(vcard_data2)) .unwrap(); let _ = server.handle(req).await; // REPORT addressbook-multiget let report_body = r#" /addressbooks/my-contacts/contact1.vcf /addressbooks/my-contacts/contact2.vcf "#; let req = Request::builder() .method("REPORT") .uri("/addressbooks/my-contacts") .body(Body::from(report_body)) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), StatusCode::MULTI_STATUS); let body_str = resp_to_string(resp).await; assert!( body_str.contains("address-data"), "Response body missing 'address-data': {}", body_str ); assert!( body_str.contains("John Doe"), "Response body missing 'John Doe': {}", body_str ); assert!( body_str.contains("Jane Smith"), "Response body missing 'Jane Smith': {}", body_str ); } #[test] fn test_is_vcard_data() { let valid_vcard = b"BEGIN:VCARD\nVERSION:3.0\nFN:Test\nEND:VCARD\n"; assert!(is_vcard_data(valid_vcard)); let invalid_data = b"This is not vcard data"; assert!(!is_vcard_data(invalid_data)); } #[test] fn test_extract_vcard_uid() { // Standard UID let vcard_with_uid = "BEGIN:VCARD\nUID:test-123@example.com\nEND:VCARD"; assert_eq!( extract_vcard_uid(vcard_with_uid), Some("test-123@example.com".to_string()) ); // UID with group prefix (e.g., "item1.UID:...") let vcard_grouped_uid = "BEGIN:VCARD\nitem1.UID:grouped-uid@example.com\nEND:VCARD"; assert_eq!( extract_vcard_uid(vcard_grouped_uid), Some("grouped-uid@example.com".to_string()) ); // UID with parameters let vcard_uid_with_params = "BEGIN:VCARD\nUID;VALUE=TEXT:param-uid@example.com\nEND:VCARD"; assert_eq!( extract_vcard_uid(vcard_uid_with_params), Some("param-uid@example.com".to_string()) ); // UID with group and parameters let vcard_grouped_with_params = "BEGIN:VCARD\nitem2.UID;VALUE=TEXT:full-uid@example.com\nEND:VCARD"; assert_eq!( extract_vcard_uid(vcard_grouped_with_params), Some("full-uid@example.com".to_string()) ); // Case insensitive let vcard_lowercase = "BEGIN:VCARD\nuid:lowercase@example.com\nEND:VCARD"; assert_eq!( extract_vcard_uid(vcard_lowercase), Some("lowercase@example.com".to_string()) ); let vcard_without_uid = "BEGIN:VCARD\nFN:Test\nEND:VCARD"; assert_eq!(extract_vcard_uid(vcard_without_uid), None); } #[test] fn test_extract_vcard_fn() { // Standard FN let vcard_with_fn = "BEGIN:VCARD\nFN:John Doe\nEND:VCARD"; assert_eq!( extract_vcard_fn(vcard_with_fn), Some("John Doe".to_string()) ); // FN with group prefix let vcard_grouped_fn = "BEGIN:VCARD\nitem1.FN:Jane Smith\nEND:VCARD"; assert_eq!( extract_vcard_fn(vcard_grouped_fn), Some("Jane Smith".to_string()) ); // FN with parameters let vcard_fn_with_params = "BEGIN:VCARD\nFN;CHARSET=UTF-8:Müller\nEND:VCARD"; assert_eq!( extract_vcard_fn(vcard_fn_with_params), Some("Müller".to_string()) ); let vcard_without_fn = "BEGIN:VCARD\nUID:test\nEND:VCARD"; assert_eq!(extract_vcard_fn(vcard_without_fn), None); } #[test] fn test_addressbook_properties_default() { let props = AddressBookProperties::default(); assert_eq!(props.max_resource_size, Some(1024 * 1024)); } #[test] fn test_validate_vcard_data() { let valid_vcard = r#"BEGIN:VCARD VERSION:3.0 UID:test@example.com FN:Test Contact N:Contact;Test;;; END:VCARD"#; assert!(validate_vcard_data(valid_vcard).is_ok()); } #[test] fn test_validate_vcard_strict() { // Valid vCard with all required properties let valid_vcard = r#"BEGIN:VCARD VERSION:3.0 UID:test@example.com FN:Test Contact N:Contact;Test;;; END:VCARD"#; assert!(validate_vcard_strict(valid_vcard).is_ok()); // Missing FN property let missing_fn = r#"BEGIN:VCARD VERSION:3.0 UID:test@example.com N:Contact;Test;;; END:VCARD"#; let result = validate_vcard_strict(missing_fn); assert!(result.is_err()); assert!(result.unwrap_err().contains("FN")); // Missing VERSION property let missing_version = r#"BEGIN:VCARD FN:Test Contact END:VCARD"#; let result = validate_vcard_strict(missing_version); assert!(result.is_err()); assert!(result.unwrap_err().contains("VERSION")); } } #[cfg(not(feature = "carddav"))] mod carddav_disabled_tests { use dav_server::{DavHandler, body::Body, fakels::FakeLs, memfs::MemFs}; use http::Request; #[tokio::test] async fn test_carddav_methods_return_not_implemented() { let server = DavHandler::builder() .filesystem(MemFs::new()) .locksystem(FakeLs::new()) .build_handler(); // Test MKADDRESSBOOK method let req = Request::builder() .method("MKADDRESSBOOK") .uri("/addressbooks/my-contacts") .body(Body::empty()) .unwrap(); let resp = server.handle(req).await; assert_eq!(resp.status(), http::StatusCode::NOT_IMPLEMENTED); } }