source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "assert_cmd"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e"
+dependencies = [
+ "bstr",
+ "doc-comment",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "assert_fs"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf09bb72e00da477c2596865e8873227e2196d263cca35414048875dbbeea1be"
+dependencies = [
+ "doc-comment",
+ "globwalk",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "tempfile",
+]
+
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote",
+ "syn",
+]
+
[[package]]
name = "async-channel"
version = "1.6.1"
"zstd-safe",
]
+[[package]]
+name = "async-executor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "once_cell",
+ "slab",
+]
+
[[package]]
name = "async-fs"
version = "1.5.0"
"futures-lite",
]
+[[package]]
+name = "async-global-executor"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd8b508d585e01084059b60f06ade4cb7415cd2e4084b71dd1cb44e7d3fb9880"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
+dependencies = [
+ "concurrent-queue",
+ "futures-lite",
+ "libc",
+ "log",
+ "once_cell",
+ "parking",
+ "polling",
+ "slab",
+ "socket2",
+ "waker-fn",
+ "winapi 0.3.9",
+]
+
[[package]]
name = "async-lock"
version = "2.5.0"
"event-listener",
]
+[[package]]
+name = "async-std"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52580991739c5cdb36cde8b2a516371c0a3b70dda36d916cc08b82372916808c"
+dependencies = [
+ "async-attributes",
+ "async-channel",
+ "async-global-executor",
+ "async-io",
+ "async-lock",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "num_cpus",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
[[package]]
name = "async-task"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9"
+[[package]]
+name = "async-trait"
+version = "0.1.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "async-walkdir"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+[[package]]
+name = "bit-set"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.2"
"once_cell",
]
+[[package]]
+name = "bstr"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+dependencies = [
+ "lazy_static",
+ "memchr",
+ "regex-automata",
+]
+
[[package]]
name = "bumpalo"
version = "3.10.0"
"cache-padded",
]
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
[[package]]
name = "cpufeatures"
version = "0.2.2"
"cfg-if",
]
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
+dependencies = [
+ "cfg-if",
+ "lazy_static",
+]
+
[[package]]
name = "crypto-common"
version = "0.1.3"
"typenum",
]
+[[package]]
+name = "ctor"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "diff"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
- "block-buffer",
+ "block-buffer 0.10.2",
"crypto-common",
]
+[[package]]
+name = "digest_auth"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa30657988b2ced88f68fe490889e739bf98d342916c33ed3100af1d6f1cbc9c"
+dependencies = [
+ "digest 0.9.0",
+ "hex",
+ "md-5",
+ "rand 0.8.5",
+ "sha2",
+]
+
+[[package]]
+name = "diqwest"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3cebabb6a61cb79191ab4cfbe1ebed9d2357f20bfd224855ec5d02bd1076c52"
+dependencies = [
+ "async-trait",
+ "digest_auth",
+ "reqwest",
+]
+
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
[[package]]
name = "duf"
version = "0.15.1"
dependencies = [
+ "assert_cmd",
+ "assert_fs",
"async-walkdir",
"async_zip",
"base64",
"chrono",
"clap",
+ "diqwest",
"env_logger",
"futures",
"get_if_addrs",
"md5",
"mime_guess",
"percent-encoding",
+ "port_check",
+ "predicates",
+ "pretty_assertions",
+ "regex",
+ "reqwest",
+ "rstest",
"rustls",
- "rustls-pemfile",
+ "rustls-pemfile 1.0.0",
+ "select",
"serde",
"serde_json",
"tokio",
"tokio-rustls",
"tokio-stream",
"tokio-util",
+ "url",
"urlencoding",
"uuid",
"xml-rs",
]
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "env_logger"
version = "0.9.0"
"miniz_oxide",
]
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[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.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
[[package]]
name = "futures"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
+[[package]]
+name = "futures-timer"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
+
[[package]]
name = "futures-util"
version = "0.3.21"
[[package]]
name = "getrandom"
-version = "0.2.6"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
- "wasi 0.10.0+wasi-snapshot-preview1",
+ "wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
-name = "hashbrown"
-version = "0.11.2"
+name = "getrandom"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+]
[[package]]
-name = "headers"
-version = "0.3.7"
+name = "globset"
+version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d"
+checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
dependencies = [
- "base64",
- "bitflags",
- "bytes",
- "headers-core",
- "http",
- "httpdate",
- "mime",
- "sha-1",
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
]
[[package]]
-name = "headers-core"
-version = "0.2.0"
+name = "globwalk"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
+checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
- "http",
+ "bitflags",
+ "ignore",
+ "walkdir",
]
[[package]]
-name = "hermit-abi"
-version = "0.1.19"
+name = "gloo-timers"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9"
dependencies = [
- "libc",
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
]
[[package]]
-name = "http"
-version = "0.2.8"
+name = "h2"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "headers"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d"
+dependencies = [
+ "base64",
+ "bitflags",
+ "bytes",
+ "headers-core",
+ "http",
+ "httpdate",
+ "mime",
+ "sha-1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
+dependencies = [
+ "http",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "html5ever"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "http"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
"futures-channel",
"futures-core",
"futures-util",
+ "h2",
"http",
"http-body",
"httparse",
"want",
]
+[[package]]
+name = "hyper-rustls"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac"
+dependencies = [
+ "http",
+ "hyper",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
+dependencies = [
+ "crossbeam-utils",
+ "globset",
+ "lazy_static",
+ "log",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
[[package]]
name = "indexmap"
version = "1.8.2"
"cfg-if",
]
+[[package]]
+name = "ipnet"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
+
+[[package]]
+name = "itertools"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.2"
"wasm-bindgen",
]
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
+[[package]]
+name = "lock_api"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
[[package]]
name = "log"
version = "0.4.17"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
+ "value-bag",
]
[[package]]
"pkg-config",
]
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
+dependencies = [
+ "log",
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "markup5ever_rcdom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b"
+dependencies = [
+ "html5ever",
+ "markup5ever",
+ "tendril",
+ "xml5ever",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "md-5"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
+dependencies = [
+ "block-buffer 0.9.0",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
[[package]]
name = "md5"
version = "0.7.0"
"windows-sys",
]
+[[package]]
+name = "native-tls"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "openssl"
+version = "0.10.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "os_str_bytes"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
+[[package]]
+name = "output_vt100"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
+dependencies = [
+ "winapi 0.3.9",
+]
+
[[package]]
name = "parking"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+[[package]]
+name = "polling"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "log",
+ "wepoll-ffi",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "port_check"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6519412c9e0d4be579b9f0618364d19cb434b324fc6ddb1b27b1e682c7105ed"
+
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "predicates"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c"
+dependencies = [
+ "difflib",
+ "float-cmp",
+ "itertools",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "pretty_assertions"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89f989ac94207d048d92db058e4f6ec7342b0971fc58d1271ca148b799b3563"
+dependencies = [
+ "ansi_term",
+ "ctor",
+ "diff",
+ "output_vt100",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.39"
]
[[package]]
-name = "rand"
-version = "0.8.5"
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom 0.2.6",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "mime_guess",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls",
+ "rustls-pemfile 0.3.0",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-rustls",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+ "winreg",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
+ "cc",
"libc",
- "rand_chacha",
- "rand_core",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi 0.3.9",
]
[[package]]
-name = "rand_chacha"
-version = "0.3.1"
+name = "rstest"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+checksum = "b939295f93cb1d12bc1a83cf9ee963199b133fb8a79832dd51b68bb9f59a04dc"
dependencies = [
- "ppv-lite86",
- "rand_core",
+ "async-std",
+ "futures",
+ "futures-timer",
+ "rstest_macros",
+ "rustc_version",
]
[[package]]
-name = "rand_core"
-version = "0.6.3"
+name = "rstest_macros"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+checksum = "f78aba848123782ba59340928ec7d876ebe745aa0365d6af8a630f19a5c16116"
dependencies = [
- "getrandom",
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
]
[[package]]
-name = "ring"
-version = "0.16.20"
+name = "rustc_version"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
- "cc",
- "libc",
- "once_cell",
- "spin",
- "untrusted",
- "web-sys",
- "winapi 0.3.9",
+ "semver",
]
[[package]]
"webpki",
]
+[[package]]
+name = "rustls-pemfile"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360"
+dependencies = [
+ "base64",
+]
+
[[package]]
name = "rustls-pemfile"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
[[package]]
name = "sct"
version = "0.7.0"
"untrusted",
]
+[[package]]
+name = "security-framework"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "select"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee061f90afcc8678bef7a78d0d121683f0ba753f740ff7005f833ec445876b7"
+dependencies = [
+ "bit-set",
+ "html5ever",
+ "markup5ever_rcdom",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c"
+
[[package]]
name = "serde"
version = "1.0.137"
"serde",
]
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
[[package]]
name = "sha-1"
version = "0.10.0"
dependencies = [
"cfg-if",
"cpufeatures",
- "digest",
+ "digest 0.10.3",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
]
[[package]]
"libc",
]
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
[[package]]
name = "slab"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
[[package]]
name = "socket2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+[[package]]
+name = "string_cache"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08"
+dependencies = [
+ "new_debug_unreachable",
+ "once_cell",
+ "parking_lot",
+ "phf_shared 0.10.0",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro2",
+ "quote",
+]
+
[[package]]
name = "syn"
version = "1.0.96"
"unicode-ident",
]
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "termtree"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
+
[[package]]
name = "textwrap"
version = "0.15.0"
"syn",
]
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
[[package]]
name = "time"
version = "0.1.44"
"winapi 0.3.9",
]
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
[[package]]
name = "tokio"
version = "1.19.2"
"syn",
]
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
[[package]]
name = "tokio-rustls"
version = "0.23.4"
"version_check",
]
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
[[package]]
name = "unicode-ident"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
[[package]]
name = "urlencoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821"
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
[[package]]
name = "uuid"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238"
dependencies = [
- "getrandom",
- "rand",
+ "getrandom 0.2.6",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "value-bag"
+version = "1.0.0-alpha.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55"
+dependencies = [
+ "ctor",
+ "version_check",
]
+[[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.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "waker-fn"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi 0.3.9",
+ "winapi-util",
+]
+
[[package]]
name = "want"
version = "0.3.0"
"try-lock",
]
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
"wasm-bindgen-shared",
]
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.80"
"untrusted",
]
+[[package]]
+name = "webpki-roots"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf"
+dependencies = [
+ "webpki",
+]
+
+[[package]]
+name = "wepoll-ffi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi 0.3.9",
+]
+
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi 0.3.9",
+]
+
[[package]]
name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
+[[package]]
+name = "xml5ever"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "time",
+]
+
[[package]]
name = "xz2"
version = "0.1.7"
license = "MIT OR Apache-2.0"
homepage = "https://github.com/sigoden/duf"
repository = "https://github.com/sigoden/duf"
-autotests = false
categories = ["command-line-utilities", "web-programming::http-server"]
keywords = ["static", "file", "server", "webdav", "cli"]
env_logger = { version = "0.9", default-features = false, features = ["humantime"] }
log = "0.4"
+[dev-dependencies]
+assert_cmd = "2"
+reqwest = { version = "0.11", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
+assert_fs = "1"
+select = "0.5"
+port_check = "0.1"
+rstest = "0.13"
+regex = "1"
+pretty_assertions = "1.2"
+url = "2"
+diqwest = { version = "1", features = ["blocking"] }
+predicates = "2"
+
[profile.release]
lto = true
strip = true
// Load public certificate from file.
fn load_certs(filename: &str) -> BoxResult<Vec<Certificate>> {
// Open certificate file.
- let certfile =
- fs::File::open(&filename).map_err(|e| format!("Failed to open {}: {}", &filename, e))?;
+ let certfile = fs::File::open(&filename)
+ .map_err(|e| format!("Failed to access `{}`, {}", &filename, e))?;
let mut reader = io::BufReader::new(certfile);
// Load and return certificate.
// Load private key from file.
fn load_private_key(filename: &str) -> BoxResult<PrivateKey> {
// Open keyfile.
- let keyfile =
- fs::File::open(&filename).map_err(|e| format!("Failed to open {}: {}", &filename, e))?;
+ let keyfile = fs::File::open(&filename)
+ .map_err(|e| format!("Failed to access `{}`, {}", &filename, e))?;
let mut reader = io::BufReader::new(keyfile);
// Load and return a single private key.
}
let req_path = req.uri().path();
+ let headers = req.headers();
+ let method = req.method().clone();
+
+ if req_path == "/favicon.ico" && method == Method::GET {
+ self.handle_send_favicon(req.headers(), &mut res).await?;
+ return Ok(res);
+ }
let path = match self.extract_path(req_path) {
Some(v) => v,
status!(res, StatusCode::NOT_FOUND);
return Ok(res);
}
- if is_miss && path.ends_with("favicon.ico") {
- *res.body_mut() = Body::from(FAVICON_ICO);
- res.headers_mut()
- .insert("content-type", "image/x-icon".parse().unwrap());
- return Ok(res);
- }
-
- let headers = req.headers();
- let method = req.method().clone();
match method {
Method::GET | Method::HEAD => {
status!(res, StatusCode::NOT_FOUND);
}
}
- "MKCOL" if allow_upload && is_miss => self.handle_mkcol(path, &mut res).await?,
- "COPY" if allow_upload && !is_miss => {
- self.handle_copy(path, headers, &mut res).await?
+ "MKCOL" => {
+ if !allow_upload || !is_miss {
+ status!(res, StatusCode::FORBIDDEN);
+ } else {
+ self.handle_mkcol(path, &mut res).await?;
+ }
+ }
+ "COPY" => {
+ if !allow_upload {
+ status!(res, StatusCode::FORBIDDEN);
+ } else if is_miss {
+ status!(res, StatusCode::NOT_FOUND);
+ } else {
+ self.handle_copy(path, headers, &mut res).await?
+ }
}
- "MOVE" if allow_upload && allow_delete && !is_miss => {
- self.handle_move(path, headers, &mut res).await?
+ "MOVE" => {
+ if !allow_upload || !allow_delete {
+ status!(res, StatusCode::FORBIDDEN);
+ } else if is_miss {
+ status!(res, StatusCode::NOT_FOUND);
+ } else {
+ self.handle_move(path, headers, &mut res).await?
+ }
}
"LOCK" => {
// Fake lock
) -> BoxResult<()> {
ensure_path_parent(path).await?;
- let mut file = fs::File::create(&path).await?;
+ let mut file = match fs::File::create(&path).await {
+ Ok(v) => v,
+ Err(_) => {
+ status!(res, StatusCode::FORBIDDEN);
+ return Ok(());
+ }
+ };
let body_with_io_error = req
.body_mut()
Ok(())
}
+ async fn handle_send_favicon(
+ &self,
+ headers: &HeaderMap<HeaderValue>,
+ res: &mut Response,
+ ) -> BoxResult<()> {
+ let path = self.args.path.join("favicon.ico");
+ let meta = fs::metadata(&path).await.ok();
+ let is_file = meta.map(|v| v.is_file()).unwrap_or_default();
+ if is_file {
+ self.handle_send_file(path.as_path(), headers, false, res)
+ .await?;
+ } else {
+ *res.body_mut() = Body::from(FAVICON_ICO);
+ res.headers_mut()
+ .insert("content-type", "image/x-icon".parse().unwrap());
+ }
+ Ok(())
+ }
+
async fn handle_send_file(
&self,
path: &Path,
return Ok(());
}
},
- None => 0,
+ None => 1,
};
let mut paths = vec![self.to_pathitem(path, &self.args.path).await?.unwrap()];
- if depth > 0 {
+ if depth != 0 {
match self.list_dir(path, &self.args.path).await {
Ok(child) => paths.extend(child),
Err(_) => {
let meta = fs::symlink_metadata(path).await?;
if meta.is_dir() {
- status!(res, StatusCode::BAD_REQUEST);
+ status!(res, StatusCode::FORBIDDEN);
return Ok(());
}
r#"
<title>Files in {}/ - Duf</title>
<style>{}</style>
-<script>var DATA = {}; {}</script>
+<script>
+const DATA =
+{}
+{}</script>
"#,
rel_path.display(),
INDEX_CSS,
PathType::Dir | PathType::SymlinkDir => None,
PathType::File | PathType::SymlinkFile => Some(meta.len()),
};
- let base_name = rel_path
- .file_name()
- .and_then(|v| v.to_str())
- .unwrap_or("/")
- .to_owned();
let name = normalize_path(rel_path);
Ok(Some(PathItem {
path_type,
- base_name,
name,
mtime,
size,
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
struct PathItem {
path_type: PathType,
- base_name: String,
name: String,
mtime: u64,
size: Option<u64>,
pub fn to_dav_xml(&self, prefix: &str) -> String {
let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822();
let href = encode_uri(&format!("{}{}", prefix, &self.name));
- let displayname = escape_str_pcdata(&self.base_name);
+ let displayname = escape_str_pcdata(self.base_name());
match self.path_type {
PathType::Dir | PathType::SymlinkDir => format!(
r#"<D:response>
),
}
}
+ fn base_name(&self) -> &str {
+ Path::new(&self.name)
+ .file_name()
+ .and_then(|v| v.to_str())
+ .unwrap_or_default()
+ }
}
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
let addrs = retrieve_listening_addrs(addr);
let protocol = if tls { "https" } else { "http" };
if addrs.len() == 1 {
- eprintln!("Listening on {}://{}{}", protocol, addr, prefix);
+ println!("Listening on {}://{}{}", protocol, addr, prefix);
} else {
- eprintln!("Listening on:");
- for addr in addrs {
- eprintln!(" {}://{}{}", protocol, addr, prefix);
- }
- eprintln!();
+ let message = addrs
+ .iter()
+ .map(|addr| format!(" {}://{}{}", protocol, addr, prefix))
+ .collect::<Vec<String>>()
+ .join("\n");
+ println!("Listening on:\n{}\n", message);
}
}
--- /dev/null
+mod fixtures;
+mod utils;
+
+use fixtures::{server, Error, TestServer};
+use rstest::rstest;
+
+#[rstest]
+fn default_not_allow_upload(server: TestServer) -> Result<(), Error> {
+ let url = format!("{}file1", server.url());
+ let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
+ assert_eq!(resp.status(), 403);
+ Ok(())
+}
+
+#[rstest]
+fn default_not_allow_delete(server: TestServer) -> Result<(), Error> {
+ let url = format!("{}test.html", server.url());
+ let resp = fetch!(b"DELETE", &url).send()?;
+ assert_eq!(resp.status(), 403);
+ Ok(())
+}
+
+#[rstest]
+fn default_not_exist_dir(server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}404/", server.url()))?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn allow_upload_not_exist_dir(
+ #[with(&["--allow-upload"])] server: TestServer,
+) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}404/", server.url()))?;
+ assert_eq!(resp.status(), 200);
+ Ok(())
+}
+
+#[rstest]
+fn allow_upload_no_override(#[with(&["--allow-upload"])] server: TestServer) -> Result<(), Error> {
+ let url = format!("{}index.html", server.url());
+ let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
+ assert_eq!(resp.status(), 403);
+ Ok(())
+}
+
+#[rstest]
+fn allow_delete_no_override(#[with(&["--allow-delete"])] server: TestServer) -> Result<(), Error> {
+ let url = format!("{}index.html", server.url());
+ let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
+ assert_eq!(resp.status(), 403);
+ Ok(())
+}
+
+#[rstest]
+fn allow_upload_delete_can_override(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let url = format!("{}index.html", server.url());
+ let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
+ assert_eq!(resp.status(), 201);
+ Ok(())
+}
--- /dev/null
+mod fixtures;
+mod utils;
+
+use diqwest::blocking::WithDigestAuth;
+use fixtures::{server, Error, TestServer};
+use rstest::rstest;
+
+#[rstest]
+fn no_auth(#[with(&["--auth", "user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(server.url())?;
+ assert_eq!(resp.status(), 401);
+ assert!(resp.headers().contains_key("www-authenticate"));
+ let url = format!("{}file1", server.url());
+ let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
+ assert_eq!(resp.status(), 401);
+ Ok(())
+}
+
+#[rstest]
+fn auth(#[with(&["--auth", "user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
+ let url = format!("{}file1", server.url());
+ let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
+ assert_eq!(resp.status(), 401);
+ let resp = fetch!(b"PUT", &url)
+ .body(b"abc".to_vec())
+ .send_with_digest_auth("user", "pass")?;
+ assert_eq!(resp.status(), 201);
+ Ok(())
+}
+
+#[rstest]
+fn auth_skip_access(
+ #[with(&["--auth", "user:pass", "--no-auth-access"])] server: TestServer,
+) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(server.url())?;
+ assert_eq!(resp.status(), 200);
+ Ok(())
+}
--- /dev/null
+mod fixtures;
+
+use fixtures::{port, server, tmpdir, Error, TestServer};
+
+use assert_cmd::prelude::*;
+use assert_fs::fixture::TempDir;
+use regex::Regex;
+use rstest::rstest;
+use std::io::{BufRead, BufReader};
+use std::process::{Command, Stdio};
+
+#[rstest]
+#[case(&["-b", "20.205.243.166"])]
+fn bind_fails(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> {
+ Command::cargo_bin("duf")?
+ .env("RUST_LOG", "false")
+ .arg(tmpdir.path())
+ .arg("-p")
+ .arg(port.to_string())
+ .args(args)
+ .assert()
+ .stderr(predicates::str::contains("creating server listener"))
+ .failure();
+
+ Ok(())
+}
+
+#[rstest]
+fn bind_ipv4(server: TestServer) -> Result<(), Error> {
+ assert!(reqwest::blocking::get(format!("http://127.0.0.1:{}", server.port()).as_str()).is_ok());
+ Ok(())
+}
+
+#[rstest]
+fn bind_ipv6(#[with(&["-b", "::"])] server: TestServer) -> Result<(), Error> {
+ assert_eq!(
+ reqwest::blocking::get(format!("http://127.0.0.1:{}", server.port()).as_str()).is_ok(),
+ !cfg!(windows)
+ );
+ assert!(reqwest::blocking::get(format!("http://[::1]:{}", server.port()).as_str()).is_ok());
+ Ok(())
+}
+
+#[rstest]
+#[case(&[] as &[&str])]
+#[case(&["--path-prefix", "/prefix"])]
+fn validate_printed_urls(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> {
+ let mut child = Command::cargo_bin("duf")?
+ .env("RUST_LOG", "false")
+ .arg(tmpdir.path())
+ .arg("-p")
+ .arg(port.to_string())
+ .args(args)
+ .stdout(Stdio::piped())
+ .spawn()?;
+
+ // WARN assumes urls list is terminated by an empty line
+ let url_lines = BufReader::new(child.stdout.take().unwrap())
+ .lines()
+ .map(|line| line.expect("Error reading stdout"))
+ .take_while(|line| !line.is_empty()) /* non-empty lines */
+ .collect::<Vec<_>>();
+ let url_lines = url_lines.join("\n");
+
+ let urls = Regex::new(r"http://[a-zA-Z0-9\.\[\]:/]+")
+ .unwrap()
+ .captures_iter(url_lines.as_str())
+ .map(|caps| caps.get(0).unwrap().as_str())
+ .collect::<Vec<_>>();
+
+ assert!(!urls.is_empty());
+
+ for url in urls {
+ reqwest::blocking::get(url)?.error_for_status()?;
+ }
+
+ child.kill()?;
+
+ Ok(())
+}
--- /dev/null
+mod fixtures;
+mod utils;
+
+use fixtures::{server, Error, TestServer};
+use rstest::rstest;
+
+#[rstest]
+fn cors(#[with(&["--cors"])] server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(server.url())?;
+
+ assert_eq!(
+ resp.headers().get("access-control-allow-origin").unwrap(),
+ "*"
+ );
+ assert_eq!(
+ resp.headers().get("access-control-allow-headers").unwrap(),
+ "range, content-type, accept, origin, www-authenticate"
+ );
+
+ Ok(())
+}
+
+#[rstest]
+fn cors_options(#[with(&["--cors"])] server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"OPTIONS", server.url()).send()?;
+
+ assert_eq!(
+ resp.headers().get("access-control-allow-origin").unwrap(),
+ "*"
+ );
+ assert_eq!(
+ resp.headers().get("access-control-allow-headers").unwrap(),
+ "range, content-type, accept, origin, www-authenticate"
+ );
+
+ Ok(())
+}
--- /dev/null
+-----BEGIN CERTIFICATE-----
+MIIFCTCCAvGgAwIBAgIUcegjikATvwNSIbN43QybKWIcKSMwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDYxMTA4NTQyMloXDTMyMDYw
+ODA4NTQyMlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEAo2wdMbFPkX7CAF/Y+hVj5bwm4dlxhwW2Z9Ic2RZFC5w2
+oK2XwyasDBEqDlgv/bN4xObAVlDZ/4/SuTVSDrNB8dtQl7GTWptpbFKJUdNocU88
+wqd4k/cLZg2aiQqnZKD88w/AxXnYw+F8yU0pFGj9GX0S5at3/V1hrBVxVO8Y99bb
+gnJA8NMm0Pw2xYZS++ULuzoECk0xbNdtbtPrIuweI5mMvsJvtiw67EIdl3N9Lj5p
+L4a7X1C0Xk5H4mOcwM0qq3m31HsCW91PMCjU6suo764rx5Jqv0n9HCNxdiSEadCw
+f+GrmKtFOw3DcGPETg5AJR8H3rG1agKKjI+vRtL/tZ7coFOhZKXdjGvvUFcWcqO+
+GppHh16pzJDXi2qeD9Cu5b2ayM2uBnfV7Q3FjOeDqD+BCJ0ClaqNmAD9TF2htzdu
+Inl+G3OJb4cqaYjaF5YmiZISfrimK5eR2I3et5cqnbuDHMKvDfUd9Jgj/2IqPOHJ
+EguuXSO7WNKfQmlTv7EN/xrD6jiB/M8ADaSxjCqTbtKNyCbJlu2Wy9WlDXwPkNW8
+g70T4Br4U4Iy3N/0w2lAAhiizdC2jkehSKmWE2nmixGSXxkSOMgXQXDJ9RBtDQfd
+8ym/ADfyVndUSnHvf9jCH1NPHlFbB7RVSvUHX22Qq63NUvhV32ct+/IyD/qPpl0C
+AwEAAaNTMFEwHQYDVR0OBBYEFKwSSbPXBIkmzja3/cNJyqhWy96WMB8GA1UdIwQY
+MBaAFKwSSbPXBIkmzja3/cNJyqhWy96WMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADggIBAHcrdu1nGDN5YvcHXzbBx73AC921fmn5xxzeFRO7af157g5h
+4zornLMk4Obp+UGkMbWK4K0NAQXKKm5WjcmoOHNRg7TgTE7b1gcVuS4phdwlIqA6
+eZGg+NWZyeaIJNjdHgWgGoe+S+5Ne1I7sDKiEXrOzITJrDcQgBKFF08kqT6UNY2W
+q90m+olPtrewAMgWllpxJ90u4qifPcwP+neDZJim9MhVYtHHeFsmyzlS185iasj8
+sxvp5HDTopmz0tDuiLHvOMKmyf7vapsnbqEGngQi2qV9rBmldyRLnWSe8u/FN31f
+zhSk1ikSm1cQ/iyL898XexSmTafyaF8ELswdIMHkGZkVQurWeKn3/CEDXokXkpMI
+4dlCSgM7SU+XtcjtXbR8/pHpcW2ZnBR0la/qIv81aNKkJeUkTcPC8BUv4jI/oT6z
+LRrvRjMnHJjnADACuutlNRU4/e7h1XuvlXgFHsp63k7GJXouoIwdHjfkErZXsoEX
+WeS+pPatkT7wbhfgYVwglMRIpgCu++htSRCV/lbSuYzCG6mKtxJyy4eslSjpHNPG
+wELDKgzsgLtuTyNfP458O9i8x6wf9J6eVaHe3nqgqkOnnmQxEYnsPaFUMWG1/DYi
+U+mA/VdQrPe3J4Z082sCe4MVmTzWlWCDpNFFQpv51NbWzc/kuIZuJCAwoZD0
+-----END CERTIFICATE-----
--- /dev/null
+#!/usr/bin/env bash
+openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -keyout key_pkcs8.pem -out cert.pem -nodes -days 3650
+openssl rsa -in key_pkcs8.pem -out key_pkcs1.pem
--- /dev/null
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
--- /dev/null
+-----BEGIN PRIVATE KEY-----
+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
+afHJpnY0yZEqN1PeDaKVOCQAHbD8qQ==
+-----END PRIVATE KEY-----
--- /dev/null
+mod fixtures;
+mod utils;
+
+use fixtures::{server, Error, TestServer};
+use rstest::rstest;
+
+#[rstest]
+fn default_favicon(server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}favicon.ico", server.url()))?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(resp.headers().get("content-type").unwrap(), "image/x-icon");
+ Ok(())
+}
+
+#[rstest]
+fn exist_favicon(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let url = format!("{}favicon.ico", server.url());
+ let data = b"abc";
+ let resp = fetch!(b"PUT", &url).body(data.to_vec()).send()?;
+ assert_eq!(resp.status(), 201);
+ let resp = reqwest::blocking::get(url)?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(resp.bytes()?, data.to_vec());
+ Ok(())
+}
--- /dev/null
+use assert_cmd::prelude::*;
+use assert_fs::fixture::TempDir;
+use assert_fs::prelude::*;
+use port_check::free_local_port;
+use reqwest::Url;
+use rstest::fixture;
+use std::process::{Child, Command, Stdio};
+use std::thread::sleep;
+use std::time::{Duration, Instant};
+
+#[allow(dead_code)]
+pub type Error = Box<dyn std::error::Error>;
+
+/// File names for testing purpose
+#[allow(dead_code)]
+pub static FILES: &[&str] = &[
+ "test.txt",
+ "test.html",
+ "index.html",
+ "test.mkv",
+ #[cfg(not(windows))]
+ "test \" \' & < >.csv",
+ "😀.data",
+ "⎙.mp4",
+ "#[]{}()@!$&'`+,;= %20.test",
+ #[cfg(unix)]
+ ":?#[]{}<>()@!$&'`|*+,;= %20.test",
+ #[cfg(not(windows))]
+ "foo\\bar.test",
+];
+
+/// Directory names for testing purpose
+#[allow(dead_code)]
+pub static DIR_NO_INDEX: &str = "dir-no-index/";
+
+/// Directory names for testing purpose
+#[allow(dead_code)]
+pub static DIRECTORIES: &[&str] = &["dira/", "dirb/", "dirc/", DIR_NO_INDEX];
+
+/// Name of a deeply nested file
+#[allow(dead_code)]
+pub static DEEPLY_NESTED_FILE: &str = "very/deeply/nested/test.rs";
+
+/// Test fixture which creates a temporary directory with a few files and directories inside.
+/// The directories also contain files.
+#[fixture]
+#[allow(dead_code)]
+pub fn tmpdir() -> TempDir {
+ let tmpdir = assert_fs::TempDir::new().expect("Couldn't create a temp dir for tests");
+ for file in FILES {
+ tmpdir
+ .child(file)
+ .write_str(&format!("This is {}", file))
+ .expect("Couldn't write to file");
+ }
+ for directory in DIRECTORIES {
+ for file in FILES {
+ if *directory == DIR_NO_INDEX {
+ continue;
+ }
+ tmpdir
+ .child(format!("{}{}", directory, file))
+ .write_str(&format!("This is {}{}", directory, file))
+ .expect("Couldn't write to file");
+ }
+ }
+
+ tmpdir
+ .child(&DEEPLY_NESTED_FILE)
+ .write_str("File in a deeply nested directory.")
+ .expect("Couldn't write to file");
+ tmpdir
+}
+
+/// Get a free port.
+#[fixture]
+#[allow(dead_code)]
+pub fn port() -> u16 {
+ free_local_port().expect("Couldn't find a free local port")
+}
+
+/// Run miniserve as a server; Start with a temporary directory, a free port and some
+/// optional arguments then wait for a while for the server setup to complete.
+#[fixture]
+#[allow(dead_code)]
+pub fn server<I>(#[default(&[] as &[&str])] args: I) -> TestServer
+where
+ I: IntoIterator + Clone,
+ I::Item: AsRef<std::ffi::OsStr>,
+{
+ let port = port();
+ let tmpdir = tmpdir();
+ let child = Command::cargo_bin("duf")
+ .expect("Couldn't find test binary")
+ .env("RUST_LOG", "false")
+ .arg(tmpdir.path())
+ .arg("-p")
+ .arg(port.to_string())
+ .args(args.clone())
+ .stdout(Stdio::null())
+ .spawn()
+ .expect("Couldn't run test binary");
+ let is_tls = args
+ .into_iter()
+ .any(|x| x.as_ref().to_str().unwrap().contains("tls"));
+
+ wait_for_port(port);
+ TestServer::new(port, tmpdir, child, is_tls)
+}
+
+/// Same as `server()` but ignore stderr
+#[fixture]
+#[allow(dead_code)]
+pub fn server_no_stderr<I>(#[default(&[] as &[&str])] args: I) -> TestServer
+where
+ I: IntoIterator + Clone,
+ I::Item: AsRef<std::ffi::OsStr>,
+{
+ let port = port();
+ let tmpdir = tmpdir();
+ let child = Command::cargo_bin("duf")
+ .expect("Couldn't find test binary")
+ .env("RUST_LOG", "false")
+ .arg(tmpdir.path())
+ .arg("-p")
+ .arg(port.to_string())
+ .args(args.clone())
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .spawn()
+ .expect("Couldn't run test binary");
+ let is_tls = args
+ .into_iter()
+ .any(|x| x.as_ref().to_str().unwrap().contains("tls"));
+
+ wait_for_port(port);
+ TestServer::new(port, tmpdir, child, is_tls)
+}
+
+/// Wait a max of 1s for the port to become available.
+fn wait_for_port(port: u16) {
+ let start_wait = Instant::now();
+
+ while !port_check::is_port_reachable(format!("localhost:{}", port)) {
+ sleep(Duration::from_millis(100));
+
+ if start_wait.elapsed().as_secs() > 1 {
+ panic!("timeout waiting for port {}", port);
+ }
+ }
+}
+
+#[allow(dead_code)]
+pub struct TestServer {
+ port: u16,
+ tmpdir: TempDir,
+ child: Child,
+ is_tls: bool,
+}
+
+#[allow(dead_code)]
+impl TestServer {
+ pub fn new(port: u16, tmpdir: TempDir, child: Child, is_tls: bool) -> Self {
+ Self {
+ port,
+ tmpdir,
+ child,
+ is_tls,
+ }
+ }
+
+ pub fn url(&self) -> Url {
+ let protocol = if self.is_tls { "https" } else { "http" };
+ Url::parse(&format!("{}://localhost:{}", protocol, self.port)).unwrap()
+ }
+
+ pub fn path(&self) -> &std::path::Path {
+ self.tmpdir.path()
+ }
+
+ pub fn port(&self) -> u16 {
+ self.port
+ }
+}
+
+impl Drop for TestServer {
+ fn drop(&mut self) {
+ self.child.kill().expect("Couldn't kill test server");
+ self.child.wait().unwrap();
+ }
+}
--- /dev/null
+mod fixtures;
+mod utils;
+
+use fixtures::{server, Error, TestServer};
+use rstest::rstest;
+
+#[rstest]
+fn get_dir(server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(server.url())?;
+ assert_index_resp!(resp);
+ Ok(())
+}
+
+#[rstest]
+fn head_dir(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"HEAD", server.url()).send()?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(
+ resp.headers().get("content-type").unwrap(),
+ "text/html; charset=utf-8"
+ );
+ assert_eq!(resp.text()?, "");
+ Ok(())
+}
+
+#[rstest]
+fn get_dir_404(server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}404/", server.url()))?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn head_dir_404(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"HEAD", format!("{}404/", server.url())).send()?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn get_dir_zip(server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}?zip", server.url()))?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(
+ resp.headers().get("content-type").unwrap(),
+ "application/zip"
+ );
+ assert!(resp.headers().contains_key("content-disposition"));
+ Ok(())
+}
+
+#[rstest]
+fn head_dir_zip(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"HEAD", format!("{}?zip", server.url())).send()?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(
+ resp.headers().get("content-type").unwrap(),
+ "application/zip"
+ );
+ assert!(resp.headers().contains_key("content-disposition"));
+ assert_eq!(resp.text()?, "");
+ Ok(())
+}
+
+#[rstest]
+fn get_dir_search(server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}?q={}", server.url(), "test.html"))?;
+ assert_eq!(resp.status(), 200);
+ let paths = utils::retrive_index_paths(&resp.text()?);
+ assert!(!paths.is_empty());
+ for p in paths {
+ assert!(p.contains(&"test.html"));
+ }
+ Ok(())
+}
+
+#[rstest]
+fn head_dir_search(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"HEAD", format!("{}?q={}", server.url(), "test.html")).send()?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(
+ resp.headers().get("content-type").unwrap(),
+ "text/html; charset=utf-8"
+ );
+ assert_eq!(resp.text()?, "");
+ Ok(())
+}
+
+#[rstest]
+fn get_file(server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}index.html", server.url()))?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(resp.headers().get("content-type").unwrap(), "text/html");
+ assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
+ assert!(resp.headers().contains_key("etag"));
+ assert!(resp.headers().contains_key("last-modified"));
+ assert!(resp.headers().contains_key("content-length"));
+ assert_eq!(resp.text()?, "This is index.html");
+ Ok(())
+}
+
+#[rstest]
+fn head_file(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"HEAD", format!("{}index.html", server.url())).send()?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(resp.headers().get("content-type").unwrap(), "text/html");
+ assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
+ assert!(resp.headers().contains_key("etag"));
+ assert!(resp.headers().contains_key("last-modified"));
+ assert!(resp.headers().contains_key("content-length"));
+ assert_eq!(resp.text()?, "");
+ Ok(())
+}
+
+#[rstest]
+fn get_file_404(server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}404", server.url()))?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn head_file_404(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"HEAD", format!("{}404", server.url())).send()?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn options_dir(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"OPTIONS", format!("{}index.html", server.url())).send()?;
+ assert_eq!(resp.status(), 204);
+ assert_eq!(
+ resp.headers().get("allow").unwrap(),
+ "GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"
+ );
+ assert_eq!(resp.headers().get("dav").unwrap(), "1");
+ Ok(())
+}
+
+#[rstest]
+fn put_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let url = format!("{}file1", server.url());
+ let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
+ assert_eq!(resp.status(), 201);
+ let resp = reqwest::blocking::get(url)?;
+ assert_eq!(resp.status(), 200);
+ Ok(())
+}
+
+#[rstest]
+fn put_file_create_dir(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let url = format!("{}xyz/file1", server.url());
+ let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
+ assert_eq!(resp.status(), 201);
+ let resp = reqwest::blocking::get(url)?;
+ assert_eq!(resp.status(), 200);
+ Ok(())
+}
+
+#[rstest]
+fn put_file_conflict_dir(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let url = format!("{}dira", server.url());
+ let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
+ assert_eq!(resp.status(), 403);
+ Ok(())
+}
+
+#[rstest]
+fn delete_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let url = format!("{}test.html", server.url());
+ let resp = fetch!(b"DELETE", &url).send()?;
+ assert_eq!(resp.status(), 204);
+ let resp = reqwest::blocking::get(url)?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn delete_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"DELETE", format!("{}file1", server.url())).send()?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
--- /dev/null
+mod fixtures;
+mod utils;
+
+use fixtures::{server, Error, TestServer};
+use rstest::rstest;
+
+#[rstest]
+fn path_prefix_index(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}{}", server.url(), "xyz"))?;
+ assert_index_resp!(resp);
+ Ok(())
+}
+
+#[rstest]
+fn path_prefix_file(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}{}/index.html", server.url(), "xyz"))?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(resp.text()?, "This is index.html");
+ Ok(())
+}
+
+#[rstest]
+fn path_prefix_propfind(
+ #[with(&["--path-prefix", "xyz"])] server: TestServer,
+) -> Result<(), Error> {
+ let resp = fetch!(b"PROPFIND", format!("{}{}", server.url(), "xyz")).send()?;
+ let text = resp.text()?;
+ assert!(text.contains("<D:href>/xyz/</D:href>"));
+ Ok(())
+}
--- /dev/null
+mod fixtures;
+
+use fixtures::{server, Error, TestServer, DIR_NO_INDEX};
+use rstest::rstest;
+
+#[rstest]
+fn render_index(#[with(&["--render-index"])] server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(server.url())?;
+ let text = resp.text()?;
+ assert_eq!(text, "This is index.html");
+ Ok(())
+}
+
+#[rstest]
+fn render_index_404(#[with(&["--render-index"])] server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}/{}", server.url(), DIR_NO_INDEX))?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn render_spa(#[with(&["--render-spa"])] server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(server.url())?;
+ let text = resp.text()?;
+ assert_eq!(text, "This is index.html");
+ Ok(())
+}
+
+#[rstest]
+fn render_spa_no_404(#[with(&["--render-spa"])] server: TestServer) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}/{}", server.url(), DIR_NO_INDEX))?;
+ let text = resp.text()?;
+ assert_eq!(text, "This is index.html");
+ Ok(())
+}
--- /dev/null
+mod fixtures;
+mod utils;
+
+use assert_fs::fixture::TempDir;
+use fixtures::{server, tmpdir, Error, TestServer};
+use rstest::rstest;
+
+#[cfg(unix)]
+use std::os::unix::fs::symlink as symlink_dir;
+#[cfg(windows)]
+use std::os::windows::fs::symlink_dir;
+
+#[rstest]
+fn default_not_allow_symlink(server: TestServer, tmpdir: TempDir) -> Result<(), Error> {
+ // Create symlink directory "foo" to point outside the root
+ let dir = "foo";
+ symlink_dir(tmpdir.path(), server.path().join(dir)).expect("Couldn't create symlink");
+ let resp = reqwest::blocking::get(format!("{}{}", server.url(), dir))?;
+ assert_eq!(resp.status(), 404);
+ let resp = reqwest::blocking::get(format!("{}{}/index.html", server.url(), dir))?;
+ assert_eq!(resp.status(), 404);
+ let resp = reqwest::blocking::get(server.url())?;
+ let paths = utils::retrive_index_paths(&resp.text()?);
+ assert!(!paths.is_empty());
+ assert!(!paths.contains(&format!("{}/", dir)));
+ Ok(())
+}
+
+#[rstest]
+fn allow_symlink(
+ #[with(&["--allow-symlink"])] server: TestServer,
+ tmpdir: TempDir,
+) -> Result<(), Error> {
+ // Create symlink directory "foo" to point outside the root
+ let dir = "foo";
+ symlink_dir(tmpdir.path(), server.path().join(dir)).expect("Couldn't create symlink");
+ let resp = reqwest::blocking::get(format!("{}{}", server.url(), dir))?;
+ assert_eq!(resp.status(), 200);
+ let resp = reqwest::blocking::get(format!("{}{}/index.html", server.url(), dir))?;
+ assert_eq!(resp.status(), 200);
+ let resp = reqwest::blocking::get(server.url())?;
+ let paths = utils::retrive_index_paths(&resp.text()?);
+ assert!(!paths.is_empty());
+ assert!(paths.contains(&format!("{}/", dir)));
+ Ok(())
+}
--- /dev/null
+mod fixtures;
+mod utils;
+
+use assert_cmd::Command;
+use fixtures::{server, Error, TestServer};
+use predicates::str::contains;
+use reqwest::blocking::ClientBuilder;
+use rstest::rstest;
+
+/// Can start the server with TLS and receive encrypted responses.
+#[rstest]
+#[case(server(&[
+ "--tls-cert", "tests/data/cert.pem",
+ "--tls-key", "tests/data/key_pkcs8.pem",
+]))]
+#[case(server(&[
+ "--tls-cert", "tests/data/cert.pem",
+ "--tls-key", "tests/data/key_pkcs1.pem",
+]))]
+fn tls_works(#[case] server: TestServer) -> Result<(), Error> {
+ let client = ClientBuilder::new()
+ .danger_accept_invalid_certs(true)
+ .build()?;
+ let resp = client.get(server.url()).send()?.error_for_status()?;
+ assert_index_resp!(resp);
+ Ok(())
+}
+
+/// Wrong path for cert throws error.
+#[rstest]
+fn wrong_path_cert() -> Result<(), Error> {
+ Command::cargo_bin("duf")?
+ .args(&["--tls-cert", "wrong", "--tls-key", "tests/data/key.pem"])
+ .assert()
+ .failure()
+ .stderr(contains("error: Failed to access `wrong`"));
+
+ Ok(())
+}
+
+/// Wrong paths for key throws errors.
+#[rstest]
+fn wrong_path_key() -> Result<(), Error> {
+ Command::cargo_bin("duf")?
+ .args(&["--tls-cert", "tests/data/cert.pem", "--tls-key", "wrong"])
+ .assert()
+ .failure()
+ .stderr(contains("error: Failed to access `wrong`"));
+
+ Ok(())
+}
--- /dev/null
+use serde_json::Value;
+use std::collections::HashSet;
+
+#[macro_export]
+macro_rules! assert_index_resp {
+ ($resp:ident) => {
+ assert_index_resp!($resp, self::fixtures::FILES)
+ };
+ ($resp:ident, $files:expr) => {
+ assert_eq!($resp.status(), 200);
+ let body = $resp.text()?;
+ let paths = self::utils::retrive_index_paths(&body);
+ assert!(!paths.is_empty());
+ for file in $files {
+ assert!(paths.contains(&file.to_string()));
+ }
+ };
+}
+
+#[macro_export]
+macro_rules! fetch {
+ ($method:literal, $url:expr) => {
+ reqwest::blocking::Client::new().request(hyper::Method::from_bytes($method)?, $url)
+ };
+}
+
+#[allow(dead_code)]
+pub fn retrive_index_paths(index: &str) -> HashSet<String> {
+ retrive_index_paths_impl(index).unwrap_or_default()
+}
+
+#[allow(dead_code)]
+pub fn encode_uri(v: &str) -> String {
+ let parts: Vec<_> = v.split('/').map(urlencoding::encode).collect();
+ parts.join("/")
+}
+
+fn retrive_index_paths_impl(index: &str) -> Option<HashSet<String>> {
+ let lines: Vec<&str> = index.lines().collect();
+ let (i, _) = lines
+ .iter()
+ .enumerate()
+ .find(|(_, v)| v.contains("const DATA"))?;
+ let line = lines.get(i + 1)?;
+ let value: Value = line.parse().ok()?;
+ let paths = value
+ .get("paths")?
+ .as_array()?
+ .iter()
+ .flat_map(|v| {
+ let name = v.get("name")?.as_str()?;
+ let path_type = v.get("path_type")?.as_str()?;
+ if path_type.ends_with("Dir") {
+ Some(format!("{}/", name))
+ } else {
+ Some(name.to_owned())
+ }
+ })
+ .collect();
+ Some(paths)
+}
--- /dev/null
+mod fixtures;
+mod utils;
+
+use fixtures::{server, Error, TestServer, FILES};
+use rstest::rstest;
+use xml::escape::escape_str_pcdata;
+
+#[rstest]
+fn propfind_dir(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"PROPFIND", format!("{}dira", server.url())).send()?;
+ assert_eq!(resp.status(), 207);
+ let body = resp.text()?;
+ assert!(body.contains("<D:href>/dira</D:href>"));
+ assert!(body.contains("<D:displayname>dira</D:displayname>"));
+ for f in FILES {
+ assert!(body.contains(&format!("<D:href>/dira/{}</D:href>", utils::encode_uri(f))));
+ assert!(body.contains(&format!(
+ "<D:displayname>{}</D:displayname>",
+ escape_str_pcdata(f)
+ )));
+ }
+ Ok(())
+}
+
+#[rstest]
+fn propfind_dir_depth0(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"PROPFIND", format!("{}dira", server.url()))
+ .header("depth", "0")
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let body = resp.text()?;
+ assert!(body.contains("<D:href>/dira</D:href>"));
+ assert!(body.contains("<D:displayname>dira</D:displayname>"));
+ assert_eq!(
+ body.lines()
+ .filter(|v| *v == "<D:status>HTTP/1.1 200 OK</D:status>")
+ .count(),
+ 1
+ );
+ Ok(())
+}
+
+#[rstest]
+fn propfind_404(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"PROPFIND", format!("{}404", server.url())).send()?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn propfind_file(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"PROPFIND", format!("{}test.html", server.url())).send()?;
+ assert_eq!(resp.status(), 207);
+ let body = resp.text()?;
+ assert!(body.contains("<D:href>/test.html</D:href>"));
+ assert!(body.contains("<D:displayname>test.html</D:displayname>"));
+ assert_eq!(
+ body.lines()
+ .filter(|v| *v == "<D:status>HTTP/1.1 200 OK</D:status>")
+ .count(),
+ 1
+ );
+ Ok(())
+}
+
+#[rstest]
+fn proppatch_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"PROPPATCH", format!("{}test.html", server.url())).send()?;
+ assert_eq!(resp.status(), 207);
+ let body = resp.text()?;
+ assert!(body.contains("<D:href>/test.html</D:href>"));
+ Ok(())
+}
+
+#[rstest]
+fn proppatch_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"PROPPATCH", format!("{}404", server.url())).send()?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn mkcol_dir(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"MKCOL", format!("{}newdir", server.url())).send()?;
+ assert_eq!(resp.status(), 201);
+ Ok(())
+}
+
+#[rstest]
+fn mkcol_not_allow_upload(server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"MKCOL", format!("{}newdir", server.url())).send()?;
+ assert_eq!(resp.status(), 403);
+ Ok(())
+}
+
+#[rstest]
+fn copy_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let new_url = format!("{}test2.html", server.url());
+ let resp = fetch!(b"COPY", format!("{}test.html", server.url()))
+ .header("Destination", &new_url)
+ .send()?;
+ assert_eq!(resp.status(), 204);
+ let resp = reqwest::blocking::get(new_url)?;
+ assert_eq!(resp.status(), 200);
+ Ok(())
+}
+
+#[rstest]
+fn copy_not_allow_upload(server: TestServer) -> Result<(), Error> {
+ let new_url = format!("{}test2.html", server.url());
+ let resp = fetch!(b"COPY", format!("{}test.html", server.url()))
+ .header("Destination", &new_url)
+ .send()?;
+ assert_eq!(resp.status(), 403);
+ Ok(())
+}
+
+#[rstest]
+fn copy_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let new_url = format!("{}test2.html", server.url());
+ let resp = fetch!(b"COPY", format!("{}404", server.url()))
+ .header("Destination", &new_url)
+ .send()?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn move_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let origin_url = format!("{}test.html", server.url());
+ let new_url = format!("{}test2.html", server.url());
+ let resp = fetch!(b"MOVE", &origin_url)
+ .header("Destination", &new_url)
+ .send()?;
+ assert_eq!(resp.status(), 204);
+ let resp = reqwest::blocking::get(new_url)?;
+ assert_eq!(resp.status(), 200);
+ let resp = reqwest::blocking::get(origin_url)?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn move_not_allow_upload(#[with(&["--allow-delete"])] server: TestServer) -> Result<(), Error> {
+ let origin_url = format!("{}test.html", server.url());
+ let new_url = format!("{}test2.html", server.url());
+ let resp = fetch!(b"MOVE", &origin_url)
+ .header("Destination", &new_url)
+ .send()?;
+ assert_eq!(resp.status(), 403);
+ Ok(())
+}
+
+#[rstest]
+fn move_not_allow_delete(#[with(&["--allow-upload"])] server: TestServer) -> Result<(), Error> {
+ let origin_url = format!("{}test.html", server.url());
+ let new_url = format!("{}test2.html", server.url());
+ let resp = fetch!(b"MOVE", &origin_url)
+ .header("Destination", &new_url)
+ .send()?;
+ assert_eq!(resp.status(), 403);
+ Ok(())
+}
+
+#[rstest]
+fn move_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let new_url = format!("{}test2.html", server.url());
+ let resp = fetch!(b"MOVE", format!("{}404", server.url()))
+ .header("Destination", &new_url)
+ .send()?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn lock_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"LOCK", format!("{}test.html", server.url())).send()?;
+ assert_eq!(resp.status(), 200);
+ let body = resp.text()?;
+ assert!(body.contains("<D:href>/test.html</D:href>"));
+ Ok(())
+}
+
+#[rstest]
+fn lock_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"LOCK", format!("{}404", server.url())).send()?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}
+
+#[rstest]
+fn unlock_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"LOCK", format!("{}test.html", server.url())).send()?;
+ assert_eq!(resp.status(), 200);
+ Ok(())
+}
+
+#[rstest]
+fn unlock_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
+ let resp = fetch!(b"LOCK", format!("{}404", server.url())).send()?;
+ assert_eq!(resp.status(), 404);
+ Ok(())
+}