diff options
author | Lukas Stabe <lukas@stabe.de> | 2025-02-05 23:03:05 +0000 |
---|---|---|
committer | Lukas Stabe <lukas@stabe.de> | 2025-02-05 23:03:05 +0000 |
commit | 317bd6a5d42a83c9c5e874788282a6e76f638211 (patch) | |
tree | 0907869057fea553c26601aaca6d2443fcc9de97 | |
parent | Merge pull request #1470 from svenstaro/dependabot/cargo/all-dependencies-184... (diff) | |
download | miniserve-317bd6a5d42a83c9c5e874788282a6e76f638211.tar.gz miniserve-317bd6a5d42a83c9c5e874788282a6e76f638211.zip |
add read-only webdav support
-rw-r--r-- | Cargo.lock | 509 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/args.rs | 6 | ||||
-rw-r--r-- | src/config.rs | 4 | ||||
-rw-r--r-- | src/errors.rs | 3 | ||||
-rw-r--r-- | src/main.rs | 51 | ||||
-rw-r--r-- | src/webdav_fs.rs | 83 | ||||
-rw-r--r-- | tests/webdav.rs | 164 |
8 files changed, 803 insertions, 19 deletions
@@ -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]] @@ -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] + ))); +} |