Repository: svenstaro/miniserve
Pull request #1415 (2024-05-13 04:45:19)

add read-only webdav support
Adds read-only webdav support by (when indexing isn't disabled) routing OPTIONS/PROPFIND requests to a `DavHandler` from the `webdav-server` crate. Hidden files are excluded via a custom `DavFileSystem`.

svenstaro: Please rebase this!

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index edf6241..c1e1e65 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -54,7 +54,7 @@ dependencies = [
  "actix-tls",
  "actix-utils",
  "ahash",
- "base64",
+ "base64 0.22.1",
  "bitflags",
  "brotli",
  "bytes",
@@ -63,7 +63,7 @@ dependencies = [
  "encoding_rs",
  "flate2",
  "futures-core",
- "h2",
+ "h2 0.3.26",
  "http 0.2.12",
  "httparse",
  "httpdate",
@@ -139,6 +139,7 @@ dependencies = [
  "bytestring",
  "cfg-if",
  "http 0.2.12",
+ "regex",
  "regex-lite",
  "serde",
  "tracing",
@@ -231,6 +232,7 @@ dependencies = [
  "bytes",
  "bytestring",
  "cfg-if",
+ "cookie",
  "derive_more",
  "encoding_rs",
  "futures-core",
@@ -242,6 +244,7 @@ dependencies = [
  "mime",
  "once_cell",
  "pin-project-lite",
+ "regex",
  "regex-lite",
  "serde",
  "serde_json",
@@ -272,7 +275,7 @@ checksum = "456348ed9dcd72a13a1f4a660449fafdecee9ac8205552e286809eb5b0b29bd3"
 dependencies = [
  "actix-utils",
  "actix-web",
- "base64",
+ "base64 0.22.1",
  "futures-core",
  "futures-util",
  "log",
@@ -307,7 +310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
  "cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
  "once_cell",
  "version_check",
  "zerocopy",
@@ -459,6 +462,23 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "async-trait"
+version = "0.1.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.96",
+]
+
+[[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"
@@ -480,6 +500,12 @@ dependencies = [
  "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"
@@ -623,6 +649,7 @@ dependencies = [
  "iana-time-zone",
  "js-sys",
  "num-traits",
+ "serde",
  "wasm-bindgen",
  "windows-targets",
 ]
@@ -737,6 +764,27 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
 
+[[package]]
+name = "cookie"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.7"
@@ -846,6 +894,39 @@ version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728"
 
+[[package]]
+name = "dav-server"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23a9e373ca09a43ad20c0b7805fcb4b489713f049a3ee2750ed61efa72f9cde9"
+dependencies = [
+ "actix-web",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "headers",
+ "htmlescape",
+ "http 1.2.0",
+ "http-body",
+ "http-body-util",
+ "lazy_static",
+ "libc",
+ "log",
+ "lru",
+ "mime_guess",
+ "parking_lot",
+ "percent-encoding",
+ "pin-project",
+ "pin-utils",
+ "regex",
+ "time",
+ "tokio",
+ "url",
+ "uuid",
+ "xml-rs",
+ "xmltree",
+]
+
 [[package]]
 name = "deranged"
 version = "0.3.11"
@@ -907,6 +988,19 @@ dependencies = [
  "crypto-common",
 ]
 
+[[package]]
+name = "digest_auth"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3054f4e81d395e50822796c5e99ca522e6ba7be98947d6d4b0e5e61640bdb894"
+dependencies = [
+ "digest",
+ "hex",
+ "md-5",
+ "rand",
+ "sha2",
+]
+
 [[package]]
 name = "displaydoc"
 version = "0.2.5"
@@ -1010,6 +1104,27 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foldhash"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
 [[package]]
 name = "form_urlencoded"
 version = "1.2.1"
@@ -1143,10 +1258,22 @@ dependencies = [
  "cfg-if",
  "js-sys",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.13.3+wasi-0.2.2",
+ "windows-targets",
+]
+
 [[package]]
 name = "gimli"
 version = "0.31.1"
@@ -1189,7 +1316,7 @@ version = "0.13.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f7a68216437ef68f0738e48d6c7bb9e6e6a92237e001b03d838314b068f33c94"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
  "grass_compiler",
  "include_sass",
 ]
@@ -1227,6 +1354,25 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "h2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http 1.2.0",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.14.5"
@@ -1242,6 +1388,35 @@ name = "hashbrown"
 version = "0.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[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",
+ "http 1.2.0",
+ "httpdate",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
+dependencies = [
+ "http 1.2.0",
+]
 
 [[package]]
 name = "heck"
@@ -1269,6 +1444,12 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[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"
@@ -1341,6 +1522,7 @@ dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
+ "h2 0.4.7",
  "http 1.2.0",
  "http-body",
  "httparse",
@@ -1369,6 +1551,22 @@ dependencies = [
  "webpki-roots",
 ]
 
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
 [[package]]
 name = "hyper-util"
 version = "0.1.10"
@@ -1661,6 +1859,12 @@ dependencies = [
  "hashbrown 0.14.5",
 ]
 
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
 [[package]]
 name = "libc"
 version = "0.2.169"
@@ -1747,6 +1951,15 @@ version = "0.4.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
 
+[[package]]
+name = "lru"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
+dependencies = [
+ "hashbrown 0.15.2",
+]
+
 [[package]]
 name = "mac"
 version = "0.1.1"
@@ -1801,6 +2014,16 @@ dependencies = [
  "syn 2.0.96",
 ]
 
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
 [[package]]
 name = "memchr"
 version = "2.7.4"
@@ -1843,6 +2066,7 @@ dependencies = [
  "clap_mangen",
  "colored",
  "comrak",
+ "dav-server",
  "fake-tty",
  "fast_qr",
  "futures",
@@ -1861,6 +2085,7 @@ dependencies = [
  "pretty_assertions",
  "regex",
  "reqwest",
+ "reqwest_dav",
  "rstest",
  "rustls",
  "rustls-pemfile",
@@ -1871,7 +2096,7 @@ dependencies = [
  "socket2",
  "strum",
  "tar",
- "thiserror",
+ "thiserror 2.0.11",
  "tokio",
  "url",
  "zip",
@@ -1894,7 +2119,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
 dependencies = [
  "libc",
  "log",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "windows-sys 0.52.0",
 ]
 
@@ -1907,6 +2132,23 @@ dependencies = [
  "rand",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
 [[package]]
 name = "new_debug_unreachable"
 version = "1.0.6"
@@ -1958,6 +2200,50 @@ version = "1.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
 
+[[package]]
+name = "openssl"
+version = "0.10.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.96",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.12.3"
@@ -2079,6 +2365,26 @@ dependencies = [
  "siphasher 1.0.1",
 ]
 
+[[package]]
+name = "pin-project"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.96",
+]
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.16"
@@ -2218,7 +2524,7 @@ dependencies = [
  "rustc-hash",
  "rustls",
  "socket2",
- "thiserror",
+ "thiserror 2.0.11",
  "tokio",
  "tracing",
 ]
@@ -2230,14 +2536,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
 dependencies = [
  "bytes",
- "getrandom",
+ "getrandom 0.2.15",
  "rand",
  "ring",
  "rustc-hash",
  "rustls",
  "rustls-pki-types",
  "slab",
- "thiserror",
+ "thiserror 2.0.11",
  "tinyvec",
  "tracing",
  "web-time",
@@ -2293,7 +2599,7 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
 ]
 
 [[package]]
@@ -2352,22 +2658,26 @@ version = "0.12.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
 dependencies = [
- "base64",
+ "base64 0.22.1",
  "bytes",
+ "encoding_rs",
  "futures-channel",
  "futures-core",
  "futures-util",
+ "h2 0.4.7",
  "http 1.2.0",
  "http-body",
  "http-body-util",
  "hyper",
  "hyper-rustls",
+ "hyper-tls",
  "hyper-util",
  "ipnet",
  "js-sys",
  "log",
  "mime",
  "mime_guess",
+ "native-tls",
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
@@ -2379,7 +2689,9 @@ dependencies = [
  "serde_json",
  "serde_urlencoded",
  "sync_wrapper",
+ "system-configuration",
  "tokio",
+ "tokio-native-tls",
  "tokio-rustls",
  "tower",
  "tower-service",
@@ -2391,6 +2703,26 @@ dependencies = [
  "windows-registry",
 ]
 
+[[package]]
+name = "reqwest_dav"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea79cbb695b7cc877ae9c0f0eeb8468e36cd03dc9c41a93bcf237396357c7b42"
+dependencies = [
+ "async-trait",
+ "chrono",
+ "digest_auth",
+ "http 1.2.0",
+ "httpdate",
+ "reqwest",
+ "serde",
+ "serde-xml-rs",
+ "serde_derive",
+ "serde_json",
+ "tokio",
+ "url",
+]
+
 [[package]]
 name = "ring"
 version = "0.17.8"
@@ -2399,7 +2731,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 dependencies = [
  "cc",
  "cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
  "libc",
  "spin",
  "untrusted",
@@ -2546,12 +2878,44 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "select"
 version = "0.6.0"
@@ -2578,6 +2942,18 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde-xml-rs"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782"
+dependencies = [
+ "log",
+ "serde",
+ "thiserror 1.0.69",
+ "xml-rs",
+]
+
 [[package]]
 name = "serde_derive"
 version = "1.0.217"
@@ -2831,6 +3207,27 @@ dependencies = [
  "syn 2.0.96",
 ]
 
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "tar"
 version = "0.4.43"
@@ -2850,7 +3247,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
 dependencies = [
  "cfg-if",
  "fastrand",
- "getrandom",
+ "getrandom 0.2.15",
  "once_cell",
  "rustix",
  "windows-sys 0.59.0",
@@ -2892,13 +3289,33 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
 
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
 [[package]]
 name = "thiserror"
 version = "2.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 2.0.11",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.96",
 ]
 
 [[package]]
@@ -2987,6 +3404,16 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-rustls"
 version = "0.26.1"
@@ -3160,12 +3587,27 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
+[[package]]
+name = "uuid"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
+dependencies = [
+ "getrandom 0.3.1",
+]
+
 [[package]]
 name = "v_htmlescape"
 version = "0.15.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
 
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
 [[package]]
 name = "version_check"
 version = "0.9.5"
@@ -3206,6 +3648,15 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasi"
+version = "0.13.3+wasi-0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.99"
@@ -3441,6 +3892,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
+dependencies = [
+ "bitflags",
+]
+
 [[package]]
 name = "write16"
 version = "1.0.0"
@@ -3464,6 +3924,12 @@ dependencies = [
  "rustix",
 ]
 
+[[package]]
+name = "xml-rs"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4"
+
 [[package]]
 name = "xml5ever"
 version = "0.17.0"
@@ -3475,6 +3941,15 @@ dependencies = [
  "markup5ever",
 ]
 
+[[package]]
+name = "xmltree"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"
+dependencies = [
+ "xml-rs",
+]
+
 [[package]]
 name = "yansi"
 version = "1.0.1"
@@ -3587,7 +4062,7 @@ dependencies = [
  "displaydoc",
  "indexmap",
  "memchr",
- "thiserror",
+ "thiserror 2.0.11",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 7dbdbdf..b1c0855 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -33,6 +33,7 @@ clap_complete = "4"
 clap_mangen = "0.2"
 colored = "3"
 comrak = { version = "0.35", default-features = false }
+dav-server = { version = "0.7", features = ["actix-compat"] }
 fast_qr = { version = "0.12", features = ["svg"] }
 futures = "0.3"
 grass = { version = "0.13", features = ["macro"], default-features = false }
@@ -74,6 +75,7 @@ predicates = "3"
 pretty_assertions = "1.2"
 regex = "1"
 reqwest = { version = "0.12", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
+reqwest_dav = "0.1"
 rstest = "0.24"
 select = "0.6"
 url = "2"
diff --git a/src/args.rs b/src/args.rs
index 9ac6772..922e78b 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -309,6 +309,12 @@ pub struct CliArgs {
     /// and return an error instead.
     #[arg(short = 'I', long, env = "MINISERVE_DISABLE_INDEXING")]
     pub disable_indexing: bool,
+
+    /// Enable read-only WebDAV support (PROPFIND requests)
+    ///
+    /// Currently incompatible with -P|--no-symlinks (see https://github.com/messense/dav-server-rs/issues/37)
+    #[arg(long, env = "MINISERVE_ENABLE_WEBDAV", conflicts_with = "no_symlinks")]
+    pub enable_webdav: bool,
 }
 
 /// Checks whether an interface is valid, i.e. it can be parsed into an IP address
diff --git a/src/config.rs b/src/config.rs
index 984f873..5d2d7e8 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -149,6 +149,9 @@ pub struct MiniserveConfig {
     /// If enabled, indexing is disabled.
     pub disable_indexing: bool,
 
+    /// If enabled, respond to WebDAV requests (read-only).
+    pub webdav_enabled: bool,
+
     /// If set, use provided rustls config for TLS
     #[cfg(feature = "tls")]
     pub tls_rustls_config: Option<rustls::ServerConfig>,
@@ -306,6 +309,7 @@ impl MiniserveConfig {
             show_wget_footer: args.show_wget_footer,
             readme: args.readme,
             disable_indexing: args.disable_indexing,
+            webdav_enabled: args.enable_webdav,
             tls_rustls_config: tls_rustls_server_config,
             compress_response: args.compress_response,
         })
diff --git a/src/errors.rs b/src/errors.rs
index 21f8f12..99c15ff 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -24,6 +24,9 @@ Please set an explicit serve path like: `miniserve /my/path`")]
     /// In case miniserve was invoked with --no-symlinks but the serve path is a symlink
     #[error("The -P|--no-symlinks option was provided but the serve path '{0}' is a symlink")]
     NoSymlinksOptionWithSymlinkServePath(String),
+
+    #[error("The --enable-webdav option was provided, but the serve path '{0}' is a file")]
+    WebdavWithFileServePath(String),
 }
 
 #[derive(Debug, Error)]
diff --git a/src/main.rs b/src/main.rs
index 1434a0c..ccf611c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,13 +6,18 @@ use std::time::Duration;
 use actix_files::NamedFile;
 use actix_web::{
     dev::{fn_service, ServiceRequest, ServiceResponse},
-    http::header::ContentType,
+    guard,
+    http::{header::ContentType, Method},
     middleware, web, App, HttpRequest, HttpResponse, Responder,
 };
 use actix_web_httpauth::middleware::HttpAuthentication;
 use anyhow::Result;
 use clap::{crate_version, CommandFactory, Parser};
 use colored::*;
+use dav_server::{
+    actix::{DavRequest, DavResponse},
+    DavConfig, DavHandler, DavMethodSet,
+};
 use fast_qr::QRBuilder;
 use log::{error, warn};
 
@@ -27,9 +32,11 @@ mod file_utils;
 mod listing;
 mod pipe;
 mod renderer;
+mod webdav_fs;
 
 use crate::config::MiniserveConfig;
 use crate::errors::{RuntimeError, StartupError};
+use crate::webdav_fs::RestrictedFs;
 
 static STYLESHEET: &str = grass::include!("data/style.scss");
 
@@ -88,6 +95,12 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), StartupError> {
         ));
     }
 
+    if miniserve_config.webdav_enabled && miniserve_config.path.is_file() {
+        return Err(StartupError::WebdavWithFileServePath(
+            miniserve_config.path.to_string_lossy().to_string(),
+        ));
+    }
+
     let inside_config = miniserve_config.clone();
 
     let canon_path = miniserve_config
@@ -307,7 +320,9 @@ fn configure_header(conf: &MiniserveConfig) -> middleware::DefaultHeaders {
 /// This is where we configure the app to serve an index file, the file listing, or a single file.
 fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
     let dir_service = || {
-        let mut files = actix_files::Files::new("", &conf.path);
+        // use routing guard so propfind and options requests fall through to the webdav handler
+        let mut files = actix_files::Files::new("", &conf.path)
+            .guard(guard::Any(guard::Get()).or(guard::Head()));
 
         // Use specific index file if one was provided.
         if let Some(ref index_file) = conf.index {
@@ -376,6 +391,38 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
         // Handle directories
         app.service(dir_service());
     }
+
+    if conf.webdav_enabled {
+        let fs = RestrictedFs::new(&conf.path, conf.show_hidden);
+
+        let dav_server = DavHandler::builder()
+            .filesystem(fs)
+            .methods(DavMethodSet::WEBDAV_RO)
+            .hide_symlinks(conf.no_symlinks)
+            .strip_prefix(conf.route_prefix.to_owned())
+            .build_handler();
+
+        app.app_data(web::Data::new(dav_server.clone()));
+
+        app.service(
+            // actix requires tail segment to be named, even if unused
+            web::resource("/{tail}*")
+                .guard(
+                    guard::Any(guard::Options())
+                        .or(guard::Method(Method::from_bytes(b"PROPFIND").unwrap())),
+                )
+                .to(dav_handler),
+        );
+    }
+}
+
+async fn dav_handler(req: DavRequest, davhandler: web::Data<DavHandler>) -> DavResponse {
+    if let Some(prefix) = req.prefix() {
+        let config = DavConfig::new().strip_prefix(prefix);
+        davhandler.handle_with(config, req.request).await.into()
+    } else {
+        davhandler.handle(req.request).await.into()
+    }
 }
 
 async fn error_404(req: HttpRequest) -> Result<HttpResponse, RuntimeError> {
diff --git a/src/webdav_fs.rs b/src/webdav_fs.rs
new file mode 100644
index 0000000..63c9f94
--- /dev/null
+++ b/src/webdav_fs.rs
@@ -0,0 +1,83 @@
+//! Helper types and functions to allow configuring hidden files visibility
+//! for WebDAV handlers
+
+use dav_server::{davpath::DavPath, fs::*, localfs::LocalFs};
+use futures::{future::ready, StreamExt, TryFutureExt};
+use std::path::{Component, Path};
+
+/// A dav_server local filesystem backend that can be configured to deny access
+/// to files and directories with names starting with a dot.
+#[derive(Clone)]
+pub struct RestrictedFs {
+    local: Box<LocalFs>,
+    show_hidden: bool,
+}
+
+impl RestrictedFs {
+    /// Creates a new RestrictedFs serving the local path at "base".
+    /// If "show_hidden" is false, access to hidden files is prevented.
+    pub fn new<P: AsRef<Path>>(base: P, show_hidden: bool) -> Box<RestrictedFs> {
+        let local = LocalFs::new(base, false, false, false);
+        Box::new(RestrictedFs { local, show_hidden })
+    }
+}
+
+/// true if any normal component of path either starts with dot or can't be turned into a str
+fn path_has_hidden_components(path: &DavPath) -> bool {
+    path.as_pathbuf().components().any(|c| match c {
+        Component::Normal(name) => name.to_str().map_or(true, |s| s.starts_with('.')),
+        _ => false,
+    })
+}
+
+impl DavFileSystem for RestrictedFs {
+    fn open<'a>(
+        &'a self,
+        path: &'a DavPath,
+        options: OpenOptions,
+    ) -> FsFuture<'a, Box<dyn DavFile>> {
+        if !path_has_hidden_components(path) || self.show_hidden {
+            self.local.open(path, options)
+        } else {
+            Box::pin(ready(Err(FsError::NotFound)))
+        }
+    }
+
+    fn read_dir<'a>(
+        &'a self,
+        path: &'a DavPath,
+        meta: ReadDirMeta,
+    ) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
+        if self.show_hidden {
+            self.local.read_dir(path, meta)
+        } else if !path_has_hidden_components(path) {
+            Box::pin(self.local.read_dir(path, meta).map_ok(|stream| {
+                let dyn_stream: FsStream<Box<dyn DavDirEntry>> = Box::pin(stream.filter(|entry| {
+                    ready(match entry {
+                        Ok(ref e) => !e.name().starts_with(b"."),
+                        _ => false,
+                    })
+                }));
+                dyn_stream
+            }))
+        } else {
+            Box::pin(ready(Err(FsError::NotFound)))
+        }
+    }
+
+    fn metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
+        if !path_has_hidden_components(path) || self.show_hidden {
+            self.local.metadata(path)
+        } else {
+            Box::pin(ready(Err(FsError::NotFound)))
+        }
+    }
+
+    fn symlink_metadata<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
+        if !path_has_hidden_components(path) || self.show_hidden {
+            self.local.symlink_metadata(path)
+        } else {
+            Box::pin(ready(Err(FsError::NotFound)))
+        }
+    }
+}


Test patch:
diff --git a/tests/webdav.rs b/tests/webdav.rs
new file mode 100644
index 0000000..1bc7e12
--- /dev/null
+++ b/tests/webdav.rs
@@ -0,0 +1,164 @@
+#[cfg(unix)]
+use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file};
+#[cfg(windows)]
+use std::os::windows::fs::{symlink_dir, symlink_file};
+use std::process::Command;
+
+use assert_cmd::prelude::*;
+use assert_fs::TempDir;
+use predicates::str::contains;
+use reqwest::{blocking::Client, Method};
+use reqwest_dav::{
+    list_cmd::{ListEntity, ListFile, ListFolder},
+    ClientBuilder as DavClientBuilder,
+};
+use rstest::rstest;
+
+mod fixtures;
+
+use crate::fixtures::{
+    server, tmpdir, Error, TestServer, DIRECTORIES, FILES, HIDDEN_DIRECTORIES, HIDDEN_FILES,
+};
+
+#[rstest]
+#[case(server(&["--enable-webdav"]), true)]
+#[case(server(&[] as &[&str]), false)]
+fn webdav_flag_works(
+    #[case] server: TestServer,
+    #[case] should_respond: bool,
+) -> Result<(), Error> {
+    let client = Client::new();
+    let response = client
+        .request(Method::from_bytes(b"PROPFIND").unwrap(), server.url())
+        .header("Depth", "1")
+        .send()?;
+
+    assert_eq!(should_respond, response.status().is_success());
+
+    Ok(())
+}
+
+#[rstest]
+fn webdav_advertised_in_options(
+    #[with(&["--enable-webdav"])] server: TestServer,
+) -> Result<(), Error> {
+    let response = Client::new()
+        .request(Method::OPTIONS, server.url())
+        .send()?
+        .error_for_status()?;
+
+    let headers = response.headers();
+    let allow = headers.get("allow").unwrap().to_str()?;
+
+    assert!(allow.contains("OPTIONS") && allow.contains("PROPFIND"));
+    assert!(headers.get("dav").is_some());
+
+    Ok(())
+}
+
+fn list_webdav(url: url::Url, path: &str) -> Result<Vec<ListEntity>, reqwest_dav::Error> {
+    let client = DavClientBuilder::new().set_host(url.to_string()).build()?;
+
+    let rt = tokio::runtime::Runtime::new().unwrap();
+
+    rt.block_on(async { client.list(path, reqwest_dav::Depth::Number(1)).await })
+}
+
+#[rstest]
+#[case(server(&["--enable-webdav"]), false)]
+#[case(server(&["--enable-webdav", "--hidden"]), true)]
+fn webdav_respects_hidden_flag(
+    #[case] server: TestServer,
+    #[case] hidden_should_show: bool,
+) -> Result<(), Error> {
+    let list = list_webdav(server.url(), "/")?;
+
+    assert_eq!(
+        hidden_should_show,
+        list.iter().any(|el|
+            matches!(el, ListEntity::File(ListFile { href, .. }) if href.contains(HIDDEN_FILES[0]))
+        )
+    );
+
+    assert_eq!(
+        hidden_should_show,
+        list.iter().any(|el|
+            matches!(el, ListEntity::Folder(ListFolder { href, .. }) if href.contains(HIDDEN_DIRECTORIES[0]))
+        )
+    );
+
+    Ok(())
+}
+
+#[rstest]
+#[case(server(&["--enable-webdav"]), true)]
+#[should_panic]
+#[case(server(&["--enable-webdav", "--no-symlinks"]), false)]
+fn webdav_respects_no_symlink_flag(#[case] server: TestServer, #[case] symlinks_should_show: bool) {
+    // Make symlinks
+    let symlink_directory_str = "symlink_directory";
+    let symlink_directory = server.path().join(symlink_directory_str);
+    let symlinked_direcotry = server.path().join(DIRECTORIES[0]);
+    symlink_dir(symlinked_direcotry, symlink_directory).unwrap();
+
+    let symlink_filename_str = "symlink_file";
+    let symlink_filename = server.path().join(symlink_filename_str);
+    let symlinked_file = server.path().join(FILES[0]);
+    symlink_file(symlinked_file, symlink_filename).unwrap();
+
+    let list = list_webdav(server.url(), "/").unwrap();
+
+    assert_eq!(
+        symlinks_should_show,
+        list.iter().any(|el|
+            matches!(el, ListEntity::File(ListFile { href, .. }) if href.contains(symlink_filename_str))
+        ),
+    );
+
+    assert_eq!(
+        symlinks_should_show,
+        list.iter().any(|el|
+            matches!(el, ListEntity::Folder(ListFolder { href, .. }) if href.contains(symlink_directory_str))
+        ),
+    );
+
+    let list_linked = list_webdav(server.url(), &format!("/{}", symlink_directory_str));
+
+    assert_eq!(symlinks_should_show, list_linked.is_ok());
+}
+
+#[rstest]
+fn webdav_works_with_route_prefix(
+    #[with(&["--enable-webdav", "--route-prefix", "test-prefix"])] server: TestServer,
+) -> Result<(), Error> {
+    let prefixed_list = list_webdav(server.url().join("test-prefix")?, "/")?;
+
+    assert!(
+        prefixed_list.iter().any(|el|
+            matches!(el, ListEntity::Folder(ListFolder { href, .. }) if href.contains(DIRECTORIES[0]))
+        )
+    );
+
+    let root_list = list_webdav(server.url(), "/");
+
+    assert!(root_list.is_err());
+
+    Ok(())
+}
+
+// timeout is used in case the binary does not exit as expected and starts waiting for requests
+#[rstest]
+#[timeout(std::time::Duration::from_secs(1))]
+fn webdav_single_file_refuses_starting(tmpdir: TempDir) {
+    Command::cargo_bin("miniserve")
+        .unwrap()
+        .current_dir(tmpdir.path())
+        .arg(FILES[0])
+        .arg("--enable-webdav")
+        .assert()
+        .failure()
+        .stderr(contains(format!(
+            "Error: The --enable-webdav option was provided, but the serve path '{}' is a file",
+            FILES[0]
+        )));
+}