aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSven-Hendrik Haase <svenstaro@gmail.com>2025-02-06 02:03:47 +0000
committerGitHub <noreply@github.com>2025-02-06 02:03:47 +0000
commit41ccc9f0c9e3b20434389d7ddb4a922b21c8f765 (patch)
tree0907869057fea553c26601aaca6d2443fcc9de97
parentMerge pull request #1470 from svenstaro/dependabot/cargo/all-dependencies-184... (diff)
parentadd read-only webdav support (diff)
downloadminiserve-41ccc9f0c9e3b20434389d7ddb4a922b21c8f765.tar.gz
miniserve-41ccc9f0c9e3b20434389d7ddb4a922b21c8f765.zip
Merge pull request #1415 from ahti/webdav
add read-only webdav support
-rw-r--r--Cargo.lock509
-rw-r--r--Cargo.toml2
-rw-r--r--src/args.rs6
-rw-r--r--src/config.rs4
-rw-r--r--src/errors.rs3
-rw-r--r--src/main.rs51
-rw-r--r--src/webdav_fs.rs83
-rw-r--r--tests/webdav.rs164
8 files changed, 803 insertions, 19 deletions
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",
@@ -460,6 +463,23 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -482,6 +502,12 @@ dependencies = [
[[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"
@@ -623,6 +649,7 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
+ "serde",
"wasm-bindgen",
"windows-targets",
]
@@ -738,6 +765,27 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -847,6 +895,39 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -908,6 +989,19 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1011,6 +1105,27 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1143,11 +1258,23 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
]
@@ -1228,6 +1355,25 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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"
@@ -1270,6 +1445,12 @@ dependencies = [
]
[[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"
@@ -1341,6 +1522,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
+ "h2 0.4.7",
"http 1.2.0",
"http-body",
"httparse",
@@ -1370,6 +1552,22 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1662,6 +1860,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1748,6 +1952,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1802,6 +2015,16 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
]
@@ -1908,6 +2133,23 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1959,6 +2201,50 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2080,6 +2366,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -2392,6 +2704,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2399,7 +2731,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
"libc",
"spin",
"untrusted",
@@ -2547,12 +2879,44 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2579,6 +2943,18 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2832,6 +3208,27 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2850,7 +3247,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
dependencies = [
"cfg-if",
"fastrand",
- "getrandom",
+ "getrandom 0.2.15",
"once_cell",
"rustix",
"windows-sys 0.59.0",
@@ -2894,11 +3291,31 @@ 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]]
@@ -2988,6 +3405,16 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3161,12 +3588,27 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3207,6 +3649,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3442,6 +3893,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3465,6 +3925,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3476,6 +3942,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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)))
+ }
+ }
+}
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]
+ )));
+}