diff --git a/CHANGELOG.md b/CHANGELOG.md index 2391b01..3382a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ -# unreleased +# 0.4.0-rc.1 (2024-08-27) +* upgrade to `http` crate version 1.0. * bump minimum Rust to 1.70. # 0.3.6 (2022-05-01) diff --git a/Cargo.lock b/Cargo.lock index c98fa7d..46829eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -17,11 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -34,15 +40,15 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "async-compression" -version = "0.4.5" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" dependencies = [ "flate2", "futures-core", @@ -53,54 +59,48 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] [[package]] name = "base64" -version = "0.21.5" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytes" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cast" @@ -110,11 +110,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.83" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ - "libc", + "shlex", ] [[package]] @@ -124,10 +124,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "ciborium" +name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -136,15 +142,15 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", @@ -152,18 +158,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.12" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.12" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstyle", "clap_lex", @@ -171,31 +177,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" - -[[package]] -name = "core-foundation" -version = "0.9.4" +version = "0.7.2" 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.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -240,61 +230,46 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.17" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "either" -version = "1.9.0" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] -name = "encoding_rs" -version = "0.8.33" +name = "crunchy" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" -dependencies = [ - "cfg-if", -] +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] -name = "equivalent" -version = "1.0.1" +name = "either" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -302,18 +277,18 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -394,46 +369,31 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] -name = "h2" -version = "0.3.22" +name = "half" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "cfg-if", + "crunchy", ] [[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - -[[package]] -name = "hashbrown" -version = "0.14.3" +name = "hermit-abi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "htmlescape" @@ -443,9 +403,9 @@ checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" -version = "0.2.11" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -454,31 +414,44 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", "pin-project-lite", ] [[package]] name = "http-serve" -version = "0.3.6" +version = "0.4.0-rc.1" dependencies = [ "bytes", "criterion", "flate2", - "futures-channel", "futures-core", "futures-util", "htmlescape", "http", "http-body", + "http-body-util", "httparse", "httpdate", "hyper", + "hyper-util", "libc", "memchr", "mime", @@ -489,6 +462,7 @@ dependencies = [ "reqwest", "smallvec", "socket2", + "sync_wrapper", "tempfile", "tokio", "winapi", @@ -496,9 +470,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -508,26 +482,42 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.28" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", - "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", "socket2", "tokio", + "tower", "tower-service", "tracing", - "want", ] [[package]] @@ -540,16 +530,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -558,12 +538,12 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", - "rustix", + "hermit-abi 0.4.0", + "libc", "windows-sys 0.52.0", ] @@ -578,42 +558,42 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.151" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -623,9 +603,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -633,59 +613,60 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" -version = "0.8.10" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "nix" -version = "0.27.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.4.1", + "bitflags", "cfg-if", + "cfg_aliases", "libc", ] [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.32.2" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] @@ -698,9 +679,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "percent-encoding" @@ -710,18 +691,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", @@ -730,9 +711,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -742,9 +723,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -755,42 +736,42 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -798,28 +779,19 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "regex" -version = "1.10.2" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -829,9 +801,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -840,26 +812,26 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "async-compression", "base64", "bytes", - "encoding_rs", "futures-core", "futures-util", - "h2", "http", "http-body", + "http-body-util", "hyper", + "hyper-util", "ipnet", "js-sys", "log", @@ -870,7 +842,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "system-configuration", + "sync_wrapper", "tokio", "tokio-util", "tower-service", @@ -878,22 +850,22 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "windows-registry", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.1", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -902,9 +874,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -917,18 +889,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.193" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -937,11 +909,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -959,35 +932,32 @@ dependencies = [ ] [[package]] -name = "slab" -version = "0.4.9" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "syn" -version = "2.0.43" +version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", @@ -995,37 +965,25 @@ dependencies = [ ] [[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" dependencies = [ - "core-foundation-sys", - "libc", + "futures-core", ] [[package]] name = "tempfile" -version = "3.9.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1040,9 +998,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -1055,26 +1013,24 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", - "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -1083,23 +1039,43 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -1137,9 +1113,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -1149,18 +1125,18 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -1169,15 +1145,15 @@ dependencies = [ [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -1200,19 +1176,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -1225,9 +1202,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.39" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -1237,9 +1214,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1247,9 +1224,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -1260,15 +1237,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "web-sys" -version = "0.3.66" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -1292,11 +1269,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -1306,143 +1283,113 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-registry" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows-targets 0.48.5", + "windows-result", + "windows-strings", + "windows-targets", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-result" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "windows-targets 0.52.0", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-result", + "windows-targets", ] [[package]] -name = "windows-targets" +name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows-targets", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" -version = "0.52.0" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" - -[[package]] -name = "winreg" -version = "0.50.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index a4ff595..083161d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http-serve" -version = "0.3.6" +version = "0.4.0-rc.1" authors = ["Scott Lamb "] license = "MIT/Apache-2.0" readme = "README.md" @@ -8,7 +8,7 @@ description = "helpers for conditional GET, HEAD, byte range serving, and gzip c keywords = ["http", "file", "range"] categories = ["web-programming::http-server"] repository = "https://github.com/scottlamb/http-serve" -edition = "2018" +edition = "2021" rust-version = "1.70" [features] @@ -21,17 +21,17 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] bytes = "1.0" flate2 = "1.0.1" -futures-channel = "0.3.1" futures-core = { version = "0.3.1", default-features = false } futures-util = { version = "0.3.1", default-features = false } -http = "0.2.0" -http-body = "0.4" +http = "1.0.0" +http-body = "1.0.0" httpdate = "1.0.0" memchr = "2.0" mime = "0.3.7" -pin-project = "1.0.6" +pin-project = "1.1.5" smallvec = "1.4.0" -tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +sync_wrapper = "1.0.1" +tokio = { version = "1.39.0", features = ["rt-multi-thread"] } [target.'cfg(unix)'.dependencies] libc = "0.2.69" @@ -42,14 +42,17 @@ winapi = { version = "0.3.6", features = ["std", "winbase"] } [dev-dependencies] criterion = { version = "0.5", features = ["async_tokio"] } htmlescape = "0.3" +http-body-util = "0.1.0" httparse = "1.3.4" -hyper = { version = "0.14", features = ["http1", "http2", "server", "stream", "tcp"] } +hyper = { version = "1.0.0", features = ["http1", "server"] } +hyper-util = { version = "0.1.2", features = ["tokio"] } mime_guess = "2.0" -nix = { version = "0.27", default-features = false, features = ["dir", "fs"] } +nix = { version = "0.29", default-features = false, features = ["dir", "fs"] } once_cell = "1.3" -reqwest = { version = "0.11", default-features = false, features = ["gzip"] } +reqwest = { version = "0.12.7", default-features = false, features = ["gzip"] } socket2 = { version = "0.5.0" } tempfile = "3.1.0" +tokio = { version = "1.39.0", features = ["macros", "sync"] } [profile.release] debug = true diff --git a/README.md b/README.md index 9ebc24d..03024e5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![CI](https://github.com/scottlamb/http-serve/workflows/CI/badge.svg)](https://github.com/scottlamb/http-serve/actions?query=workflow%3ACI) Rust helpers for serving HTTP GET and HEAD responses with -[hyper](https://crates.io/crates/hyper) 0.14.x and +[hyper](https://crates.io/crates/hyper) 1.x and [tokio](https://crates.io/crates/tokio). This crate supplies two ways to respond to HTTP GET and HEAD requests: diff --git a/benches/file.rs b/benches/file.rs index 0e15922..be832b7 100644 --- a/benches/file.rs +++ b/benches/file.rs @@ -6,21 +6,26 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +use bytes::Bytes; use criterion::{criterion_group, criterion_main, Criterion}; use http::{Request, Response}; -use hyper::Body; +use hyper_util::rt::TokioIo; use once_cell::sync::Lazy; use std::ffi::OsString; use std::fs::File; use std::io::Write; +use std::net::SocketAddr; use std::sync::Mutex; use std::time::Duration; use tempfile::TempDir; +use tokio::net::TcpListener; -type BoxedError = Box; +type BoxError = Box; -async fn serve(req: Request) -> Result, BoxedError> { - let f = tokio::task::block_in_place::<_, Result<_, BoxedError>>(move || { +async fn serve( + req: Request, +) -> Result>, BoxError> { + let f = tokio::task::block_in_place::<_, Result<_, BoxError>>(move || { let f = std::fs::File::open(&*PATH.lock().unwrap())?; let headers = http::header::HeaderMap::new(); Ok(http_serve::ChunkedReadFile::new(f, headers)?) @@ -32,17 +37,23 @@ async fn serve(req: Request) -> Result, BoxedError> { fn new_server() -> String { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let make_svc = hyper::service::make_service_fn(|_conn| { - futures_util::future::ok::<_, hyper::Error>(hyper::service::service_fn(serve)) - }); let rt = tokio::runtime::Runtime::new().unwrap(); let _guard = rt.enter(); - - let addr = ([127, 0, 0, 1], 0).into(); - let srv = hyper::Server::bind(&addr).tcp_nodelay(true).serve(make_svc); - let addr = srv.local_addr(); - tx.send(addr).unwrap(); - rt.block_on(srv).unwrap(); + rt.block_on(async { + let addr = SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + tx.send(listener.local_addr().unwrap()).unwrap(); + loop { + let (tcp, _) = listener.accept().await.unwrap(); + let io = TokioIo::new(tcp); + tokio::task::spawn(async move { + hyper::server::conn::http1::Builder::new() + .serve_connection(io, hyper::service::service_fn(serve)) + .await + .unwrap(); + }); + } + }); }); let addr = rx.recv().unwrap(); format!("http://{}:{}", addr.ip(), addr.port()) diff --git a/benches/inmem.rs b/benches/inmem.rs index 5340c53..6f6255a 100644 --- a/benches/inmem.rs +++ b/benches/inmem.rs @@ -15,25 +15,25 @@ use futures_core::Stream; use futures_util::{future, stream}; use http::header::HeaderValue; use http::{Request, Response}; -use http_serve::streaming_body; -use hyper::Body; +use http_serve::{streaming_body, BoxError}; +use hyper_util::rt::TokioIo; use once_cell::sync::Lazy; use std::convert::TryInto; use std::io::{Read, Write}; -use std::net::SocketAddr; +use std::net::{Ipv4Addr, SocketAddr}; use std::ops::Range; +use std::pin::Pin; use std::str::FromStr; use std::time::{Duration, SystemTime}; +use tokio::net::TcpListener; static WONDERLAND: &[u8] = include_bytes!("wonderland.txt"); -type BoxedError = Box; - struct BytesEntity(Bytes); impl http_serve::Entity for BytesEntity { type Data = Bytes; - type Error = BoxedError; + type Error = BoxError; fn len(&self) -> u64 { self.0.len() as u64 @@ -41,8 +41,8 @@ impl http_serve::Entity for BytesEntity { fn get_range( &self, range: Range, - ) -> Box> + Send + Sync> { - Box::new(stream::once(future::ok( + ) -> Pin> + Send + Sync>> { + Box::pin(stream::once(future::ok( self.0 .slice(range.start as usize..range.end as usize) .into(), @@ -62,7 +62,9 @@ impl http_serve::Entity for BytesEntity { } } -async fn serve(req: Request) -> Result, BoxedError> { +type Body = http_serve::Body; + +async fn serve(req: Request) -> Result, BoxError> { let path = req.uri().path(); let resp = match path.as_bytes()[1] { b's' => { @@ -115,17 +117,24 @@ async fn serve(req: Request) -> Result, BoxedError> { fn new_server() -> SocketAddr { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let make_svc = hyper::service::make_service_fn(|_conn| { - futures_util::future::ok::<_, hyper::Error>(hyper::service::service_fn(serve)) - }); let rt = tokio::runtime::Runtime::new().unwrap(); let _guard = rt.enter(); - - let addr = ([127, 0, 0, 1], 0).into(); - let srv = hyper::Server::bind(&addr).tcp_nodelay(true).serve(make_svc); - let addr = srv.local_addr(); - tx.send(addr).unwrap(); - rt.block_on(srv).unwrap(); + rt.block_on(async { + let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + tx.send(listener.local_addr().unwrap()).unwrap(); + loop { + let (tcp, _) = listener.accept().await.unwrap(); + tokio::spawn(async move { + tcp.set_nodelay(true).unwrap(); + let io = TokioIo::new(tcp); + hyper::server::conn::http1::Builder::new() + .serve_connection(io, hyper::service::service_fn(serve)) + .await + .unwrap(); + }); + } + }); }); rx.recv().unwrap() } diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index 79b18c9..0410673 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -9,23 +9,28 @@ //! Test program which serves the current directory on `http://127.0.0.1:1337/`. //! Note this requires `--features dir`. -use futures_util::future; use http::header::{self, HeaderMap, HeaderValue}; use http_serve::dir; -use hyper::service::{make_service_fn, service_fn}; -use hyper::Body; +use hyper_util::rt::TokioIo; use std::fmt::Write; use std::io::Write as RawWrite; +use std::net::SocketAddr; use std::sync::Arc; +use tokio::net::TcpListener; + +use http_serve::Body; fn is_dir(dir: &nix::dir::Dir, ent: &nix::dir::Entry) -> Result { + // Many filesystems return file types in the directory entries. if let Some(t) = ent.file_type() { return Ok(t == nix::dir::Type::Directory); } + + // ...but some require an fstat call. use nix::sys::stat::{fstatat, SFlag}; use std::os::unix::io::AsRawFd; let stat = fstatat( - dir.as_raw_fd(), + Some(dir.as_raw_fd()), ent.file_name(), nix::fcntl::AtFlags::empty(), )?; @@ -34,7 +39,7 @@ fn is_dir(dir: &nix::dir::Dir, ent: &nix::dir::Entry) -> Result, + req: http::Request, node: dir::Node, ) -> Result, ::std::io::Error> { if node.metadata().is_dir() { @@ -45,7 +50,7 @@ fn reply( return Ok(http::Response::builder() .status(http::StatusCode::MOVED_PERMANENTLY) .header(http::header::LOCATION, loc) - .body(Body::empty()) + .body(http_serve::Body::empty()) .unwrap()); } let (mut resp, stream) = http_serve::streaming_body(&req).build(); @@ -99,16 +104,19 @@ fn reply( async fn serve( fs_dir: &Arc, - req: http::Request, + req: http::Request, ) -> Result, ::std::io::Error> { let p = if req.uri().path() == "/" { "." } else { &req.uri().path()[1..] }; - // TODO: this should go through the same unwrapping. - let node = Arc::clone(&fs_dir).get(p, req.headers()).await?; - let e = match reply(req, node) { + + let e = match Arc::clone(&fs_dir) + .get(p, req.headers()) + .await + .and_then(|node| reply(req, node)) + { Ok(res) => return Ok(res), Err(e) => e, }; @@ -118,7 +126,7 @@ async fn serve( }; Ok(http::Response::builder() .status(status) - .body(format!("I/O error: {}", e).into()) + .body(http_serve::Body::from(format!("I/O error: {}", e))) .unwrap()) } @@ -126,11 +134,18 @@ async fn serve( async fn main() { let dir: &'static Arc = Box::leak(Box::new(dir::FsDir::builder().for_path(".").unwrap())); - let addr = ([127, 0, 0, 1], 1337).into(); - let make_svc = make_service_fn(move |_conn| { - future::ok::<_, std::convert::Infallible>(service_fn(move |req| serve(dir, req))) - }); - let server = hyper::Server::bind(&addr).serve(make_svc); - println!("Serving . on http://{} with 1 thread.", server.local_addr()); - server.await.unwrap(); + let addr = SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, 1337)); + let listener = TcpListener::bind(addr).await.unwrap(); + println!("Serving . on http://{}", listener.local_addr().unwrap()); + loop { + let (tcp, _) = listener.accept().await.unwrap(); + tokio::spawn(async move { + tcp.set_nodelay(true).unwrap(); + let io = TokioIo::new(tcp); + hyper::server::conn::http1::Builder::new() + .serve_connection(io, hyper::service::service_fn(move |req| serve(dir, req))) + .await + .unwrap(); + }); + } } diff --git a/examples/serve_file.rs b/examples/serve_file.rs index 1061047..5a30692 100644 --- a/examples/serve_file.rs +++ b/examples/serve_file.rs @@ -18,24 +18,29 @@ //! $ curl -v -H 'Range: bytes=1-10,30-40' http://127.0.0.1:1337/ //! ``` +use std::net::{Ipv4Addr, SocketAddr}; + use bytes::Bytes; -use futures_util::future; use http::{ header::{self, HeaderMap, HeaderValue}, Request, Response, }; use http_serve::ChunkedReadFile; -use hyper::service::{make_service_fn, service_fn}; -use hyper::Body; +use hyper::{server::conn::http1, service::service_fn}; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; struct Context { path: std::ffi::OsString, } -type BoxedError = Box; +type BoxError = Box; -async fn serve(ctx: &'static Context, req: Request) -> Result, BoxedError> { - let f = tokio::task::block_in_place::<_, Result, BoxedError>>( +async fn serve( + ctx: &'static Context, + req: Request, +) -> Result, BoxError> { + let f = tokio::task::block_in_place::<_, Result, BoxError>>( move || { let f = std::fs::File::open(&ctx.path)?; let mut headers = HeaderMap::new(); @@ -50,7 +55,7 @@ async fn serve(ctx: &'static Context, req: Request) -> Result Result<(), BoxedError> { +async fn main() -> Result<(), BoxError> { let mut args = std::env::args_os(); if args.len() != 2 { eprintln!("Expected serve [FILENAME]"); @@ -60,17 +65,19 @@ async fn main() -> Result<(), BoxedError> { let ctx: &'static Context = Box::leak(Box::new(Context { path: path })); - let addr = ([127, 0, 0, 1], 1337).into(); - let make_svc = make_service_fn(move |_conn| { - future::ok::<_, std::convert::Infallible>(service_fn(move |req| serve(ctx, req))) - }); - let server = hyper::Server::bind(&addr).serve(make_svc); - println!( - "Serving {} on http://{} with 1 thread.", - ctx.path.to_string_lossy(), - server.local_addr() - ); - server.await?; - - Ok(()) + let addr: SocketAddr = (Ipv4Addr::LOCALHOST, 1337).into(); + let listener = TcpListener::bind(addr).await?; + println!("Serving {} on http://{}", ctx.path.to_string_lossy(), addr,); + loop { + let (tcp, _) = listener.accept().await?; + let io = TokioIo::new(tcp); + tokio::task::spawn(async move { + if let Err(e) = http1::Builder::new() + .serve_connection(io, service_fn(move |req| serve(ctx, req))) + .await + { + eprintln!("Error serving connection: {}", e); + } + }); + } } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index a37864d..a1a9cac 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -13,7 +13,8 @@ cargo-fuzz = true arbitrary = { version = "1", features = ["derive"] } bytes = "1.0.1" futures = "0.3.14" -http = "0.2.0" +http = "1.1.0" +http-body = "1.0.1" httpdate = "1.0.0" hyper = { version = "0.14.7", features = ["stream"] } libfuzzer-sys = "0.4" diff --git a/fuzz/fuzz_targets/serve_nonempty.rs b/fuzz/fuzz_targets/serve_nonempty.rs index e6a7dfe..ee2aaee 100644 --- a/fuzz/fuzz_targets/serve_nonempty.rs +++ b/fuzz/fuzz_targets/serve_nonempty.rs @@ -1,9 +1,9 @@ #![no_main] -use futures::Stream; +use http_body::Body as _; use http::header::HeaderValue; -use hyper::Body; use libfuzzer_sys::fuzz_target; use once_cell::sync::Lazy; +use std::pin::Pin; use std::task::Poll; use std::time::SystemTime; @@ -45,7 +45,7 @@ struct Req { impl http_serve::Entity for &'static FakeEntity { type Data = bytes::Bytes; - type Error = Box; + type Error = http_serve::BoxError; fn len(&self) -> u64 { BODY.len() as u64 @@ -53,8 +53,8 @@ impl http_serve::Entity for &'static FakeEntity { fn get_range( &self, range: std::ops::Range, - ) -> Box> + Send + Sync> { - Box::new(futures::stream::once(futures::future::ok( + ) -> Pin> + Send + Sync>> { + Box::pin(futures::stream::once(futures::future::ok( BODY[range.start as usize..range.end as usize].into(), ))) } @@ -99,13 +99,13 @@ fuzz_target!(|data: Req| { Ok(r) => r, }; - let response = http_serve::serve::<_, Body, _>(&*ENTITY_STRONG_ETAG, &request); - let body: Body = response.into_body(); + let response = http_serve::serve(&*ENTITY_STRONG_ETAG, &request); + let body = response.into_body(); futures::pin_mut!(body); let waker = futures::task::noop_waker(); let mut cx = futures::task::Context::from_waker(&waker); loop { - match body.as_mut().poll_next(&mut cx) { + match body.as_mut().poll_frame(&mut cx) { Poll::Pending => panic!("FakeEntity is never pending"), Poll::Ready(Some(Err(_))) => panic!("FakeEntity never serves error"), Poll::Ready(Some(Ok(_))) => {} diff --git a/src/body.rs b/src/body.rs new file mode 100644 index 0000000..e4c6d6f --- /dev/null +++ b/src/body.rs @@ -0,0 +1,293 @@ +// Copyright (c) 2023 The http-serve developers +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::{pin::Pin, task::Poll}; + +use bytes::Buf; +use futures_core::Stream; +use sync_wrapper::SyncWrapper; + +use crate::BoxError; + +/// An [`http_body::Body`] implementation returned by [`crate::serve`] and +/// [`crate::StreamingBodyBuilder::build`]. +#[pin_project::pin_project] +pub struct Body(#[pin] pub(crate) BodyStream); + +impl http_body::Body for Body +where + D: Buf + From> + From<&'static [u8]> + 'static, + E: From + 'static, +{ + type Data = D; + type Error = E; + + #[inline] + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll, Self::Error>>> { + self.as_mut() + .project() + .0 + .poll_next(cx) + .map(|p| p.map(|o| o.map(http_body::Frame::data))) + } + + fn size_hint(&self) -> http_body::SizeHint { + match &self.0 { + BodyStream::Once(Some(Ok(d))) => http_body::SizeHint::with_exact( + u64::try_from(d.remaining()).expect("usize should fit in u64"), + ), + BodyStream::Once(_) => http_body::SizeHint::with_exact(0), + BodyStream::ExactLen(l) => http_body::SizeHint::with_exact(l.remaining), + BodyStream::Multipart(s) => http_body::SizeHint::with_exact(s.remaining()), + BodyStream::Chunker(c) => c.size_hint(), + } + } + + fn is_end_stream(&self) -> bool { + match &self.0 { + BodyStream::Once(c) => c.is_none(), + BodyStream::ExactLen(l) => l.remaining == 0, + BodyStream::Multipart(s) => s.remaining() == 0, + BodyStream::Chunker(c) => c.is_end_stream(), + } + } +} + +impl Body { + /// Returns a 0-byte body. + #[inline] + pub fn empty() -> Self { + Self(BodyStream::Once(None)) + } +} + +impl From<&'static [u8]> for Body +where + D: From<&'static [u8]>, +{ + #[inline] + fn from(value: &'static [u8]) -> Self { + Self(BodyStream::Once(Some(Ok(value.into())))) + } +} + +impl From<&'static str> for Body +where + D: From<&'static [u8]>, +{ + #[inline] + fn from(value: &'static str) -> Self { + Self(BodyStream::Once(Some(Ok(value.as_bytes().into())))) + } +} + +impl From> for Body +where + D: From>, +{ + #[inline] + fn from(value: Vec) -> Self { + Self(BodyStream::Once(Some(Ok(value.into())))) + } +} + +impl From for Body +where + D: From>, +{ + #[inline] + fn from(value: String) -> Self { + Self(BodyStream::Once(Some(Ok(value.into_bytes().into())))) + } +} + +#[pin_project::pin_project(project = BodyStreamProj)] +pub(crate) enum BodyStream { + Once(Option>), + ExactLen(#[pin] ExactLenStream), + Multipart(#[pin] crate::serving::MultipartStream), + Chunker(#[pin] crate::chunker::Reader), +} + +impl Stream for BodyStream +where + D: 'static + Buf + From> + From<&'static [u8]>, + E: 'static + From, +{ + type Item = Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll>> { + match self.project() { + BodyStreamProj::Once(c) => Poll::Ready(c.take()), + BodyStreamProj::ExactLen(s) => s.poll_next(cx), + BodyStreamProj::Multipart(s) => s.poll_next(cx), + BodyStreamProj::Chunker(s) => s.poll_next(cx), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct StreamTooShortError { + remaining: u64, +} + +impl std::fmt::Display for StreamTooShortError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "stream ended with {} bytes still expected", + self.remaining + ) + } +} + +impl std::error::Error for StreamTooShortError {} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct StreamTooLongError { + extra: u64, +} + +impl std::fmt::Display for StreamTooLongError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "stream returned (at least) {} bytes more than expected", + self.extra + ) + } +} + +impl std::error::Error for StreamTooLongError {} + +pub(crate) struct ExactLenStream { + #[allow(clippy::type_complexity)] + stream: SyncWrapper> + Send>>>, + remaining: u64, +} + +impl ExactLenStream { + pub(crate) fn new(len: u64, stream: Pin> + Send>>) -> Self { + Self { + stream: SyncWrapper::new(stream), + remaining: len, + } + } +} + +impl futures_core::Stream for ExactLenStream +where + D: Buf, + E: From, +{ + type Item = Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll>> { + let this = Pin::into_inner(self); + match this.stream.get_mut().as_mut().poll_next(cx) { + Poll::Ready(Some(Ok(d))) => { + let d_len = crate::as_u64(d.remaining()); + let new_rem = this.remaining.checked_sub(d_len); + if let Some(new_rem) = new_rem { + this.remaining = new_rem; + Poll::Ready(Some(Ok(d))) + } else { + let remaining = std::mem::take(&mut this.remaining); // fuse. + Poll::Ready(Some(Err(E::from(Box::new(StreamTooLongError { + extra: d_len - remaining, + }))))) + } + } + Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))), + Poll::Ready(None) => { + if this.remaining != 0 { + let remaining = std::mem::take(&mut this.remaining); // fuse. + return Poll::Ready(Some(Err(E::from(Box::new(StreamTooShortError { + remaining, + }))))); + } + Poll::Ready(None) + } + Poll::Pending => Poll::Pending, + } + } +} + +const _: () = { + fn _assert() { + fn assert_bounds() {} + assert_bounds::(); + } +}; + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use futures_util::StreamExt as _; + + use super::*; + + #[tokio::test] + async fn correct_exact_len_stream() { + let inner = futures_util::stream::iter(vec![Ok("h".into()), Ok("ello".into())]); + let mut exact_len = + std::pin::pin!(ExactLenStream::::new(5, Box::pin(inner))); + assert_eq!(exact_len.remaining, 5); + let frame = exact_len.next().await.unwrap().unwrap(); + assert_eq!(frame.remaining(), 1); + assert_eq!(exact_len.remaining, 4); + let frame = exact_len.next().await.unwrap().unwrap(); + assert_eq!(frame.remaining(), 4); + assert_eq!(exact_len.remaining, 0); + assert!(exact_len.next().await.is_none()); // end of stream. + assert!(exact_len.next().await.is_none()); // fused. + } + + #[tokio::test] + async fn short_exact_len_stream() { + let inner = futures_util::stream::iter(vec![Ok("hello".into())]); + let mut exact_len = + std::pin::pin!(ExactLenStream::::new(10, Box::pin(inner))); + assert_eq!(exact_len.remaining, 10); + let frame = exact_len.next().await.unwrap().unwrap(); + assert_eq!(frame.remaining(), 5); + assert_eq!(exact_len.remaining, 5); + let err = exact_len.next().await.unwrap().unwrap_err(); + assert_eq!( + err.downcast_ref::().unwrap(), + &StreamTooShortError { remaining: 5 } + ); + assert!(exact_len.next().await.is_none()); // fused. + } + + #[tokio::test] + async fn long_exact_len_stream() { + let inner = futures_util::stream::iter(vec![Ok("h".into()), Ok("ello".into())]); + let mut exact_len = + std::pin::pin!(ExactLenStream::::new(3, Box::pin(inner))); + assert_eq!(exact_len.remaining, 3); + let frame = exact_len.next().await.unwrap().unwrap(); + assert_eq!(frame.remaining(), 1); + assert_eq!(exact_len.remaining, 2); + let err = exact_len.next().await.unwrap().unwrap_err(); + assert_eq!( + err.downcast_ref::().unwrap(), + &StreamTooLongError { extra: 2 } + ); + assert!(exact_len.next().await.is_none()); // fused. + } +} diff --git a/src/chunker.rs b/src/chunker.rs index ceff4e9..1c7f018 100644 --- a/src/chunker.rs +++ b/src/chunker.rs @@ -6,10 +6,127 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use futures_channel::mpsc; -use futures_core::Stream; +use std::collections::VecDeque; use std::io::{self, Write}; use std::mem; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use http_body::SizeHint; + +/// Shared between [`Writer`] and [`Reader`]. +struct Shared { + state: SharedState, + + /// The waker to notify when the receiver should be polled again. + waker: Option, +} + +enum SharedState { + Ok { + ready: VecDeque>, + + /// Current total size (`ready.iter().map(Vec::len).sum()`) of the ready queue. + ready_bytes: usize, + + writer_dropped: bool, + }, + Err(E), + ReaderFused, +} + +pub struct Reader { + shared: Arc>>, + _marker: std::marker::PhantomData, +} + +impl Reader { + pub fn size_hint(&self) -> SizeHint { + let mut h = SizeHint::default(); + if let SharedState::Ok { + ready_bytes, + writer_dropped, + .. + } = &self.shared.lock().expect("not poisoned").state + { + let r = crate::as_u64(*ready_bytes); + h.set_lower(r); + if *writer_dropped { + h.set_upper(r); + } + } + h + } + + pub fn is_end_stream(&self) -> bool { + match self.shared.lock().expect("not poisoned").state { + SharedState::Ok { + ready_bytes, + writer_dropped, + .. + } => ready_bytes == 0 && writer_dropped, + SharedState::Err(_) => false, + SharedState::ReaderFused => true, + } + } +} + +impl futures_core::Stream for Reader +where + D: From>, +{ + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let shared = &self.as_mut().shared; + let mut l = shared.lock().expect("not poisoned"); + match std::mem::replace(&mut l.state, SharedState::ReaderFused) { + SharedState::Ok { + mut ready, + mut ready_bytes, + writer_dropped, + } => { + if let Some(c) = ready.pop_front() { + ready_bytes -= c.len(); + if !ready.is_empty() || !writer_dropped { + // more chunks may follow. + l.state = SharedState::Ok { + ready, + ready_bytes, + writer_dropped, + }; + drop(l); + } + return Poll::Ready(Some(Ok(D::from(c)))); + } + + debug_assert_eq!(ready_bytes, 0); + + if !writer_dropped { + if !l.waker.as_ref().is_some_and(|w| w.will_wake(cx.waker())) { + l.waker = Some(cx.waker().clone()); + l.state = SharedState::Ok { + ready, + ready_bytes, + writer_dropped, + }; + drop(l); + return Poll::Pending; + } + return Poll::Ready(None); + } + + Poll::Ready(None) + } + SharedState::Err(e) => Poll::Ready(Some(Err(e))), + SharedState::ReaderFused => Poll::Ready(None), + } + } +} /// A `std::io::Write` implementation that makes a chunked hyper response body stream. /// Raw in the sense that it doesn't apply content encoding and isn't particularly user-friendly: @@ -23,41 +140,107 @@ use std::mem; /// The stream is infinitely buffered; calls to `write` and `flush` never block. `flush` thus is a /// hint that data should be sent to the client as soon as possible, but this shouldn't be expected /// to happen before it returns. -pub(crate) struct BodyWriter +pub(crate) struct Writer where D: From> + Send + 'static, E: Send + 'static, { - sender: mpsc::UnboundedSender>, + shared: Arc>>, - /// The next buffer to use. Invariant: capacity > len. + /// The next buffer to use. buf: Vec, + + /// Desired capacity of each buffer. + cap: usize, + + _marker: std::marker::PhantomData, } -impl BodyWriter +// impl From> for crate::Body +// where +// D: Send + 'static, +// E: Send + 'static, +// { +// fn from(value: Body) -> Self { +// crate::Body::unchecked_len_stream(Box::pin(value)) +// } +// } + +impl Writer where D: From> + Send + 'static, E: Send + 'static, { - pub(crate) fn with_chunk_size( - cap: usize, - ) -> (Self, Box> + Send>) { + pub(crate) fn with_chunk_size(cap: usize) -> (Self, Reader) { assert!(cap > 0); - let (snd, rcv) = mpsc::unbounded(); - let body = Box::new(rcv); + let shared = Arc::new(Mutex::new(Shared { + state: SharedState::Ok { + ready: VecDeque::new(), + ready_bytes: 0, + writer_dropped: false, + }, + waker: None, + })); ( - BodyWriter { - sender: snd, - buf: Vec::with_capacity(cap), + Writer { + shared: shared.clone(), + buf: Vec::new(), + cap, + _marker: std::marker::PhantomData, + }, + Reader { + shared, + _marker: std::marker::PhantomData, }, - body, ) } /// Causes the HTTP connection to be dropped abruptly with the given error. pub(crate) fn abort(&mut self, error: E) { - // hyper drops the connection when the stream contains an error. - let _ = self.sender.unbounded_send(Err(error)); + let mut l = self.shared.lock().expect("not poisoned"); + let _ready; + let waker; + if let SharedState::Ok { ready, .. } = &mut l.state { + _ready = std::mem::take(ready); // drop might be slow; release lock first. + l.state = SharedState::Err(error); + waker = l.waker.take(); + } else { + return; + }; + drop(l); + if let Some(w) = waker { + w.wake(); + } + } + + fn flush_helper(&mut self, dropping: bool) -> Result<(), ()> { + if self.buf.is_empty() && !dropping { + return Ok(()); + } + let mut l = self.shared.lock().expect("not poisoned"); + let waker = if let SharedState::Ok { + ready, + ready_bytes, + writer_dropped, + } = &mut l.state + { + if !self.buf.is_empty() { + let full_buf = mem::take(&mut self.buf); + *ready_bytes += full_buf.len(); + ready.push_back(full_buf); + } + *writer_dropped = dropping; + l.waker.take() + } else if !self.buf.is_empty() { + return Err(()); + } else { + return Ok(()); + }; + drop(l); + if let Some(w) = waker { + w.wake(); + } + Ok(()) } /// Truncates the output buffer (for testing). @@ -67,12 +250,17 @@ where } } -impl Write for BodyWriter +impl Write for Writer where D: From> + Send + 'static, E: Send + 'static, { fn write(&mut self, buf: &[u8]) -> io::Result { + if self.buf.capacity() == 0 { + self.buf.reserve_exact(self.cap); + } else { + assert!(self.buf.capacity() >= self.cap); + } let remaining = self.buf.capacity() - self.buf.len(); let full = remaining <= buf.len(); let bytes = if full { remaining } else { buf.len() }; @@ -84,56 +272,50 @@ where } fn flush(&mut self) -> io::Result<()> { - if !self.buf.is_empty() { - let cap = self.buf.capacity(); - let full_buf = mem::replace(&mut self.buf, Vec::with_capacity(cap)); - if self.sender.unbounded_send(Ok(full_buf.into())).is_err() { - // If this error is returned, no further writes will succeed either. - // Therefore, it's acceptable to just drop the full_buf (now e.into_inner()) - // rather than put it back as self.buf; it won't cause us to write a stream with - // a gap. - return Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "receiver was dropped", - )); - } - } - Ok(()) + self.flush_helper(false) + .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "receiver was dropped")) } } -impl Drop for BodyWriter +impl Drop for Writer where D: From> + Send + 'static, E: Send + 'static, { fn drop(&mut self) { - let _ = self.flush(); + let _ = self.flush_helper(true); } } #[cfg(test)] mod tests { - use super::BodyWriter; - use futures_core::Stream; + use super::Writer; use futures_util::{stream::StreamExt, stream::TryStreamExt}; use std::io::Write; - use std::pin::Pin; - type BoxedError = Box; - type BodyStream = Box, BoxedError>> + Send>; + type BoxError = Box; + type Reader = super::Reader, BoxError>; - async fn to_vec(s: BodyStream) -> Vec { - Pin::from(s).try_concat().await.unwrap() + async fn to_vec(s: Reader) -> Vec { + s.try_concat().await.unwrap() } - // A smaller-than-chunk-size write shouldn't be flushed on write, and there's currently no Drop - // implementation to do it either. + // Just dropping should end the stream. + #[tokio::test] + async fn just_drop() { + let (w, body): (_, Reader) = Writer::with_chunk_size(4); + assert!(!body.is_end_stream()); + drop(w); + assert!(body.is_end_stream()); + assert_eq!(b"", &to_vec(body).await[..]); + } + + // A smaller-than-chunk-size write shouldn't be flushed on write. #[tokio::test] async fn small_no_flush() { - let (mut w, body): (_, BodyStream) = BodyWriter::with_chunk_size(4); + let (mut w, body): (_, Reader) = Writer::with_chunk_size(4); assert_eq!(w.write(b"1").unwrap(), 1); - w.truncate(); + w.truncate(); // prevents auto-flush from doing anything. drop(w); assert_eq!(b"", &to_vec(body).await[..]); } @@ -141,9 +323,19 @@ mod tests { // With a flush, the content should show up. #[tokio::test] async fn small_flush() { - let (mut w, body): (_, BodyStream) = BodyWriter::with_chunk_size(4); + let (mut w, body): (_, Reader) = Writer::with_chunk_size(4); assert_eq!(w.write(b"1").unwrap(), 1); w.flush().unwrap(); + w.truncate(); // prevents auto-flush from doing anything. + drop(w); + assert_eq!(b"1", &to_vec(body).await[..]); + } + + // With a drop, the content should show up. + #[tokio::test] + async fn small_drop() { + let (mut w, body): (_, Reader) = Writer::with_chunk_size(4); + assert_eq!(w.write(b"1").unwrap(), 1); drop(w); assert_eq!(b"1", &to_vec(body).await[..]); } @@ -151,9 +343,9 @@ mod tests { // A write of exactly the chunk size should be automatically flushed. #[tokio::test] async fn chunk_write() { - let (mut w, body): (_, BodyStream) = BodyWriter::with_chunk_size(4); + let (mut w, body): (_, Reader) = Writer::with_chunk_size(4); assert_eq!(w.write(b"1234").unwrap(), 4); - w.flush().unwrap(); + w.truncate(); drop(w); assert_eq!(b"1234", &to_vec(body).await[..]); } @@ -161,10 +353,9 @@ mod tests { // ...and everything should be set up for the next write as well. #[tokio::test] async fn chunk_double_write() { - let (mut w, body): (_, BodyStream) = BodyWriter::with_chunk_size(4); + let (mut w, body): (_, Reader) = Writer::with_chunk_size(4); assert_eq!(w.write(b"1234").unwrap(), 4); assert_eq!(w.write(b"5678").unwrap(), 4); - w.flush().unwrap(); drop(w); assert_eq!(b"12345678", &to_vec(body).await[..]); } @@ -172,7 +363,7 @@ mod tests { // A larger-than-chunk-size write should be turned into a chunk-size write. #[tokio::test] async fn large_write() { - let (mut w, body): (_, BodyStream) = BodyWriter::with_chunk_size(4); + let (mut w, body): (_, Reader) = Writer::with_chunk_size(4); assert_eq!(w.write(b"123456").unwrap(), 4); drop(w); assert_eq!(b"1234", &to_vec(body).await[..]); @@ -181,17 +372,17 @@ mod tests { // ...similarly, one that uses all the remaining capacity of the chunk. #[tokio::test] async fn small_large_write() { - let (mut w, body): (_, BodyStream) = BodyWriter::with_chunk_size(4); + let (mut w, body): (_, Reader) = Writer::with_chunk_size(4); assert_eq!(w.write(b"1").unwrap(), 1); assert_eq!(w.write(b"2345").unwrap(), 3); drop(w); assert_eq!(b"1234", &to_vec(body).await[..]); } - // Aborting should add an Err element to the stream, ignoring any unflushed bytes. + // Aborting should cause the stream to Err, dumping anything in the queue. #[tokio::test] async fn abort() { - let (mut w, body): (_, BodyStream) = BodyWriter::with_chunk_size(4); + let (mut w, body): (_, Reader) = Writer::with_chunk_size(4); w.write_all(b"12345").unwrap(); w.truncate(); w.abort(Box::new(std::io::Error::new( @@ -199,11 +390,8 @@ mod tests { "asdf", ))); drop(w); - let items = Pin::<_>::from(body) - .collect::, BoxedError>>>() - .await; - assert_eq!(items.len(), 2); - assert_eq!(b"1234", &items[0].as_ref().unwrap()[..]); - items[1].as_ref().unwrap_err(); + let items = body.collect::, BoxError>>>().await; + assert_eq!(items.len(), 1); + items[0].as_ref().unwrap_err(); } } diff --git a/src/dir.rs b/src/dir.rs index b7601e7..18603f9 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -46,6 +46,7 @@ pub struct FsDir { } impl FsDir { + /// Builds a new `FsDir`. pub fn builder() -> FsDirBuilder { FsDirBuilder { auto_gzip: true } } diff --git a/src/file.rs b/src/file.rs index 563c4d6..154d1cb 100644 --- a/src/file.rs +++ b/src/file.rs @@ -14,6 +14,7 @@ use http::header::{HeaderMap, HeaderValue}; use std::error::Error as StdError; use std::io; use std::ops::Range; +use std::pin::Pin; use std::sync::Arc; use std::time::{self, SystemTime}; @@ -32,11 +33,10 @@ static CHUNK_SIZE: u64 = 65_536; /// /// ``` /// # use bytes::Bytes; -/// # use hyper::Body; /// # use http::{Request, Response, header::{self, HeaderMap, HeaderValue}}; -/// type BoxedError = Box; -/// async fn serve_dictionary(req: Request) -> Result, BoxedError> { -/// let f = tokio::task::block_in_place::<_, Result<_, BoxedError>>( +/// type BoxError = Box; +/// async fn serve_dictionary(req: Request) -> Result, BoxError> { +/// let f = tokio::task::block_in_place::<_, Result<_, BoxError>>( /// move || { /// let f = std::fs::File::open("/usr/share/dict/words")?; /// let mut headers = http::header::HeaderMap::new(); @@ -135,7 +135,7 @@ where fn get_range( &self, range: Range, - ) -> Box> + Send + Sync> { + ) -> Pin> + Send + Sync>> { let stream = stream::unfold( (range, Arc::clone(&self.inner)), move |(left, inner)| async { @@ -161,7 +161,7 @@ where }, ); let _: &dyn Stream> = &stream; - Box::new(stream) + Box::pin(stream) } fn add_headers(&self, h: &mut HeaderMap) { @@ -211,12 +211,12 @@ mod tests { use std::io::Write; use std::pin::Pin; - type BoxedError = Box; - type CRF = ChunkedReadFile; + type BoxError = Box; + type CRF = ChunkedReadFile; async fn to_bytes( - s: Box> + Send>, - ) -> Result { + s: Pin> + Send>>, + ) -> Result { let concat = Pin::from(s) .try_fold(Vec::new(), |mut acc, item| async move { acc.extend(&item[..]); diff --git a/src/gzip.rs b/src/gzip.rs index 8b31d63..cfaae51 100644 --- a/src/gzip.rs +++ b/src/gzip.rs @@ -36,8 +36,8 @@ where D: From> + Send + 'static, E: Send + 'static, { - Raw(chunker::BodyWriter), - Gzipped(flate2::write::GzEncoder>), + Raw(chunker::Writer), + Gzipped(flate2::write::GzEncoder>), /// No more data should be sent. `abort()` or `drop()` has been called, or a previous call /// discovered that the receiver has been dropped. @@ -49,11 +49,11 @@ where D: From> + Send + 'static, E: Send + 'static, { - pub(crate) fn raw(raw: chunker::BodyWriter) -> Self { + pub(crate) fn raw(raw: chunker::Writer) -> Self { BodyWriter(Inner::Raw(raw)) } - pub(crate) fn gzipped(raw: chunker::BodyWriter, level: flate2::Compression) -> Self { + pub(crate) fn gzipped(raw: chunker::Writer, level: flate2::Compression) -> Self { BodyWriter(Inner::Gzipped(flate2::GzBuilder::new().write(raw, level))) } diff --git a/src/lib.rs b/src/lib.rs index 16b5909..19ad7da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ //! Helpers for serving HTTP GET and HEAD responses asynchronously with the //! [http](http://crates.io/crates/http) crate and [tokio](https://crates.io/crates/tokio). -//! Works well with [hyper](https://crates.io/crates/hyper) 0.14.x. +//! Works well with [hyper](https://crates.io/crates/hyper) 1.x. //! //! This crate supplies two ways to respond to HTTP GET and HEAD requests: //! @@ -30,12 +30,12 @@ //! They have pros and cons. This table shows some of them: //! //! -//! -//! -//! -//! -//! -//! +//! +//! +//! +//! +//! +//! //!
servestreaming_body
automatic byte range servingyesno [1]
backpressureyesno [2]
conditional GETyesno [3]
sends first byte before length knownnoyes
automatic gzip content encodingno [4]yes
servestreaming_body
automatic byte range servingyesno [1]
backpressureyesno [2]
conditional GETyesno [3]
sends first byte before length knownnoyes
automatic gzip content encodingno [4]yes
//! //! \[1\]: `streaming_body` always sends the full body. Byte range serving @@ -106,13 +106,14 @@ //! when dropped. In these cases, the caller can supply an alternate implementation of the //! `http_body::Body` trait which uses a different `Data` type than `bytes::Bytes`. -#![deny(clippy::print_stderr, clippy::print_stdout)] +#![deny(missing_docs, clippy::print_stderr, clippy::print_stdout)] #![cfg_attr(docsrs, feature(doc_cfg))] use bytes::Buf; use futures_core::Stream; use http::header::{self, HeaderMap, HeaderValue}; use std::ops::Range; +use std::pin::Pin; use std::str::FromStr; use std::time::SystemTime; @@ -131,6 +132,17 @@ macro_rules! unsafe_fmt_ascii_val { }} } +fn as_u64(len: usize) -> u64 { + const { + assert!(usize::MAX as u64 <= u64::MAX); + }; + len as u64 +} + +/// A type-erased error. +pub type BoxError = Box; + +mod body; mod chunker; #[cfg(feature = "dir")] @@ -144,6 +156,7 @@ mod platform; mod range; mod serving; +pub use crate::body::Body; pub use crate::file::ChunkedReadFile; pub use crate::gzip::BodyWriter; pub use crate::serving::serve; @@ -151,12 +164,22 @@ pub use crate::serving::serve; /// A reusable, read-only, byte-rangeable HTTP entity for GET and HEAD serving. /// Must return exactly the same data on every call. pub trait Entity: 'static + Send + Sync { - type Error: 'static + Send + Sync; + /// The type of errors produced in [`Self::get_range`] chunks and in the final stream. + /// + /// [`BoxError`] is a good choice for most implementations. + /// + /// This must be convertable from [`BoxError`] to allow `http_serve` to + /// inject errors. + /// + /// Note that errors returned directly from the body to `hyper` just drop + /// the stream abruptly without being logged. Callers might use an + /// intermediary service for better observability. + type Error: 'static + From; /// The type of a data chunk. /// /// Commonly `bytes::Bytes` but may be something more exotic. - type Data: 'static + Send + Sync + Buf + From> + From<&'static [u8]>; + type Data: 'static + Buf + From> + From<&'static [u8]>; /// Returns the length of the entity's body in bytes. fn len(&self) -> u64; @@ -167,12 +190,15 @@ pub trait Entity: 'static + Send + Sync { } /// Gets the body bytes indicated by `range`. + /// + /// The stream must return exactly `range.end - range.start` bytes or fail early with an `Err`. + #[allow(clippy::type_complexity)] fn get_range( &self, range: Range, - ) -> Box> + Send + Sync>; + ) -> Pin> + Send + Sync>>; - /// Adds entity headers such as `Content-Type` to the supplied `Headers` object. + /// Adds entity headers such as `Content-Type` to the supplied `HeaderMap`. /// In particular, these headers are the "other representation header fields" described by [RFC /// 7233 section 4.1](https://tools.ietf.org/html/rfc7233#section-4.1); they should exclude /// `Content-Range`, `Date`, `Cache-Control`, `ETag`, `Expires`, `Content-Location`, and `Vary`. @@ -255,7 +281,7 @@ pub fn should_gzip(headers: &HeaderMap) -> bool { }; quality = q; } - } + }; if coding == "gzip" { gzip_q = Some(quality); @@ -277,7 +303,7 @@ pub fn should_gzip(headers: &HeaderMap) -> bool { gzip_q > 0 && gzip_q >= identity_q } -/// A builder returned by [streaming_body]. +/// A builder returned by [`streaming_body`]. pub struct StreamingBodyBuilder { chunk_size: usize, gzip_level: u32, @@ -295,7 +321,7 @@ pub struct StreamingBodyBuilder { /// # use http::{Request, Response, header::{self, HeaderValue}}; /// use std::io::Write as _; /// -/// fn respond(req: Request) -> std::io::Result> { +/// fn respond(req: Request) -> std::io::Result> { /// let (mut resp, stream) = http_serve::streaming_body(&req).build(); /// if let Some(mut w) = stream { /// write!(&mut w, "hello world")?; @@ -314,7 +340,7 @@ pub struct StreamingBodyBuilder { /// # use http::{Request, Response, header::{self, HeaderValue}}; /// use std::io::Write as _; /// -/// fn respond(req: Request) -> std::io::Result> { +/// fn respond(req: Request) -> std::io::Result> { /// let (mut resp, stream) = http_serve::streaming_body(&req).build(); /// if let Some(mut w) = stream { /// tokio::spawn(async move { @@ -329,7 +355,7 @@ pub struct StreamingBodyBuilder { /// Ok(resp) /// } /// ``` -pub fn streaming_body(req: &http::Request) -> StreamingBodyBuilder { +pub fn streaming_body(req: &H) -> StreamingBodyBuilder { StreamingBodyBuilder { chunk_size: 4096, gzip_level: 6, @@ -338,6 +364,39 @@ pub fn streaming_body(req: &http::Request) -> StreamingBodyBuilder { } } +/// A trait for types that provide the required request accessors needed by [`streaming_body`]. +pub trait AsRequest { + /// Returns the HTTP method. + fn method(&self) -> &http::Method; + + /// Returns the request headers. + fn headers(&self) -> &http::HeaderMap; +} + +impl AsRequest for http::Request { + #[inline] + fn method(&self) -> &http::Method { + self.method() + } + + #[inline] + fn headers(&self) -> &http::HeaderMap { + self.headers() + } +} + +impl AsRequest for http::request::Parts { + #[inline] + fn method(&self) -> &http::Method { + &self.method + } + + #[inline] + fn headers(&self) -> &http::HeaderMap { + &self.headers + } +} + impl StreamingBodyBuilder { /// Sets the size of a data chunk. /// @@ -358,14 +417,14 @@ impl StreamingBodyBuilder { } /// Returns the HTTP response and, if the request is a `GET`, a body writer. - pub fn build(self) -> (http::Response

, Option>) + #[allow(clippy::type_complexity)] + pub fn build(self) -> (http::Response>, Option>) where D: From> + Send + Sync, E: Send + Sync, - P: From> + Send>>, { - let (w, stream) = chunker::BodyWriter::with_chunk_size(self.chunk_size); - let mut resp = http::Response::new(stream.into()); + let (w, r) = chunker::Writer::with_chunk_size(self.chunk_size); + let mut resp = http::Response::new(Body(crate::body::BodyStream::Chunker(r))); resp.headers_mut() .append(header::VARY, HeaderValue::from_static("accept-encoding")); diff --git a/src/serving.rs b/src/serving.rs index 3ded17f..3c14d0a 100644 --- a/src/serving.rs +++ b/src/serving.rs @@ -7,20 +7,19 @@ // except according to those terms. use super::Entity; +use crate::body::Body; use crate::etag; use crate::range; +use crate::BoxError; use bytes::Buf; -use futures_core::Stream; -use futures_util::stream::{self, StreamExt}; +use futures_util::stream::StreamExt as _; use http::header::{self, HeaderMap, HeaderValue}; use http::{self, Method, Request, Response, StatusCode}; -use http_body::Body; use httpdate::{fmt_http_date, parse_http_date}; -use pin_project::pin_project; -use std::future::Future; use std::io::Write; use std::ops::Range; use std::pin::Pin; +use std::task::Poll; use std::time::SystemTime; const MAX_DECIMAL_U64_BYTES: usize = 20; // u64::max_value().to_string().len() @@ -66,34 +65,14 @@ fn parse_modified_hdrs( Ok((precondition_failed, not_modified)) } -fn static_body(s: &'static str) -> Box> + Send> -where - D: 'static + Send + Buf + From> + From<&'static [u8]>, - E: 'static + Send, -{ - Box::new(stream::once(futures_util::future::ok(s.as_bytes().into()))) -} - -fn empty_body() -> Box> + Send> -where - D: 'static + Send + Buf + From> + From<&'static [u8]>, - E: 'static + Send, -{ - Box::new(stream::empty()) -} - /// Serves GET and HEAD requests for a given byte-ranged entity. /// Handles conditional & subrange requests. /// The caller is expected to have already determined the correct entity and appended /// `Expires`, `Cache-Control`, and `Vary` headers if desired. -pub fn serve< - Ent: Entity, - B: Body + From> + Send>>, - BI, ->( +pub fn serve( entity: Ent, req: &Request, -) -> Response { +) -> Response> { // serve takes entity itself for ownership, as needed for the multipart case. But to avoid // monomorphization code bloat when there are many implementations of Entity, // delegate as much as possible to functions which take a reference to a trait object. @@ -101,45 +80,43 @@ pub fn serve< ServeInner::Simple(res) => res, ServeInner::Multipart { res, - mut part_headers, + part_headers, ranges, - } => { - let bodies = stream::unfold(0, move |state| { - next_multipart_body_chunk(state, &entity, &ranges[..], &mut part_headers[..]) - }); - let body = bodies.flatten(); - let body: Box> + Send> = Box::new(body); - res.body(body.into()).unwrap() - } + len, + } => res + .body(crate::body::Body(crate::body::BodyStream::Multipart( + MultipartStream::new(Box::new(entity), part_headers, ranges, len), + ))) + .expect("multipart response should be valid"), } } /// An instruction from `serve_inner` to `serve` on how to respond. -enum ServeInner { - Simple(Response), +enum ServeInner { + Simple(Response>), Multipart { res: http::response::Builder, part_headers: Vec>, ranges: Vec>, + len: u64, }, } /// Runs trait object-based inner logic for `serve`. fn serve_inner< - D: 'static + Send + Sync + Buf + From> + From<&'static [u8]>, - E: 'static + Send + Sync, - B: Body + From> + Send>>, + D: 'static + Buf + From> + From<&'static [u8]>, + E: 'static + From, >( ent: &dyn Entity, method: &http::Method, req_hdrs: &http::HeaderMap, -) -> ServeInner { +) -> ServeInner { if method != Method::GET && method != Method::HEAD { return ServeInner::Simple( Response::builder() .status(StatusCode::METHOD_NOT_ALLOWED) .header(header::ALLOW, HeaderValue::from_static("get, head")) - .body(static_body::("This resource only supports GET and HEAD.").into()) + .body(Body::from("This resource only supports GET and HEAD.")) .unwrap(), ); } @@ -153,7 +130,7 @@ fn serve_inner< return ServeInner::Simple( Response::builder() .status(StatusCode::BAD_REQUEST) - .body(static_body::(s).into()) + .body(Body::from(s)) .unwrap(), ) } @@ -209,15 +186,12 @@ fn serve_inner< if precondition_failed { res = res.status(StatusCode::PRECONDITION_FAILED); - return ServeInner::Simple( - res.body(static_body::("Precondition failed").into()) - .unwrap(), - ); + return ServeInner::Simple(res.body(Body::from("Precondition failed")).unwrap()); } if not_modified { res = res.status(StatusCode::NOT_MODIFIED); - return ServeInner::Simple(res.body(empty_body::().into()).unwrap()); + return ServeInner::Simple(res.body(Body::empty()).unwrap()); } let len = ent.len(); @@ -238,30 +212,39 @@ fn serve_inner< res = res.status(StatusCode::PARTIAL_CONTENT); (range.clone(), include_entity_headers_on_range) } else { - let ranges = ranges.into_vec(); - // Before serving multiple ranges via multipart/byteranges, estimate the total // length. ("80" is the RFC's estimate of the size of each part's header.) If it's // more than simply serving the whole entity, do that instead. - let est_len: u64 = ranges.iter().map(|r| 80 + r.end - r.start).sum(); - if est_len < len { - let (res, part_headers) = prepare_multipart( - res, - &ranges[..], - len, - include_entity_headers_on_range.then(|| { - let mut h = HeaderMap::new(); - ent.add_headers(&mut h); - h - }), - ); + let est_len = ranges.iter().try_fold(0u64, |acc, r| { + acc.checked_add(80) + .and_then(|a| a.checked_add(r.end - r.start)) + }); + if matches!(est_len, Some(l) if l < len) { + let each_part_hdrs = include_entity_headers_on_range.then(|| { + let mut h = HeaderMap::new(); + ent.add_headers(&mut h); + h + }); + let (res, part_headers, len) = + match prepare_multipart(res, &ranges[..], len, each_part_hdrs) { + Ok(v) => v, + Err(MultipartLenOverflowError) => { + return ServeInner::Simple( + Response::builder() + .status(StatusCode::PAYLOAD_TOO_LARGE) + .body(Body::from("Multipart response too large")) + .unwrap(), + ); + } + }; if method == Method::HEAD { - return ServeInner::Simple(res.body(empty_body::().into()).unwrap()); + return ServeInner::Simple(res.body(Body::empty()).unwrap()); } return ServeInner::Multipart { res, part_headers, - ranges, + ranges: ranges.into_vec(), + len, }; } @@ -274,56 +257,37 @@ fn serve_inner< unsafe_fmt_ascii_val!(MAX_DECIMAL_U64_BYTES + "bytes */".len(), "bytes */{}", len), ); res = res.status(StatusCode::RANGE_NOT_SATISFIABLE); - return ServeInner::Simple(res.body(empty_body::().into()).unwrap()); + return ServeInner::Simple(res.body(Body::empty()).unwrap()); } }; + let len = range.end - range.start; res = res.header( header::CONTENT_LENGTH, - unsafe_fmt_ascii_val!(MAX_DECIMAL_U64_BYTES, "{}", range.end - range.start), + unsafe_fmt_ascii_val!(MAX_DECIMAL_U64_BYTES, "{}", len), ); let body = match *method { - Method::HEAD => empty_body::(), - _ => ent.get_range(range), + Method::HEAD => Body::empty(), + _ => Body(crate::body::BodyStream::ExactLen( + crate::body::ExactLenStream::new(range.end - range.start, ent.get_range(range)), + )), }; - let mut res = res.body(body.into()).unwrap(); + let mut res = res.body(body).unwrap(); if include_entity_headers { ent.add_headers(res.headers_mut()); } ServeInner::Simple(res) } -/// A body for use in the "stream of streams" (see `prepare_multipart` and its call site). -/// This avoids an extra allocation for the part headers and overall trailer. -#[pin_project(project=InnerBodyProj)] -enum InnerBody { - Once(Option), - - // The box variant _holds_ a pin but isn't structurally pinned. - B(Pin> + Sync + Send>>), -} - -impl Stream for InnerBody { - type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - ctx: &mut std::task::Context, - ) -> std::task::Poll>> { - let mut this = self.project(); - match this { - InnerBodyProj::Once(ref mut o) => std::task::Poll::Ready(o.take().map(Ok)), - InnerBodyProj::B(b) => b.as_mut().poll_next(ctx), - } - } -} +struct MultipartLenOverflowError; /// Prepares to send a `multipart/mixed` response. -/// Returns the response builder (with overall headers added) and each part's headers. +/// Returns the response builder (with overall headers added), each part's headers, and overall len. fn prepare_multipart( mut res: http::response::Builder, ranges: &[Range], len: u64, include_entity_headers: Option, -) -> (http::response::Builder, Vec>) { +) -> Result<(http::response::Builder, Vec>, u64), MultipartLenOverflowError> { let mut each_part_headers = Vec::new(); if let Some(h) = include_entity_headers { each_part_headers.reserve( @@ -339,7 +303,7 @@ fn prepare_multipart( } } - let mut body_len = 0; + let mut body_len: u64 = 0; let mut part_headers: Vec> = Vec::with_capacity(ranges.len()); for r in ranges { let mut buf = Vec::with_capacity( @@ -358,10 +322,15 @@ fn prepare_multipart( .unwrap(); buf.extend_from_slice(&each_part_headers); buf.extend_from_slice(b"\r\n"); - body_len += buf.len() as u64 + r.end - r.start; + body_len = body_len + .checked_add(crate::as_u64(buf.len())) + .and_then(|l| l.checked_add(r.end - r.start)) + .ok_or(MultipartLenOverflowError)?; part_headers.push(buf); } - body_len += PART_TRAILER.len() as u64; + body_len = body_len + .checked_add(crate::as_u64(PART_TRAILER.len())) + .ok_or(MultipartLenOverflowError)?; res = res.header( header::CONTENT_LENGTH, @@ -373,37 +342,106 @@ fn prepare_multipart( ); res = res.status(StatusCode::PARTIAL_CONTENT); - (res, part_headers) + Ok((res, part_headers, body_len)) +} + +pub(crate) struct MultipartStream { + /// The current part's body stream, set in "send body" state. + cur: Option>, + + /// Current state: + /// + /// * `i << 1` for `i` in `[0, ranges.len())`: send part headers. + /// * `i << 1 | 1` for `i` in `[0, ranges.len())`: send body. + /// * `ranges.len() << 1`: send trailer + /// * `ranges.len() << 1 | 1`: end + state: usize, + part_headers: Vec>, + ranges: Vec>, + entity: Box>, + remaining: u64, +} + +impl MultipartStream { + pub(crate) fn new( + entity: Box>, + part_headers: Vec>, + ranges: Vec>, + len: u64, + ) -> Self { + Self { + cur: None, + state: 0, + part_headers, + ranges, + entity, + remaining: len, + } + } + + pub(crate) fn remaining(&self) -> u64 { + self.remaining + } } /// The trailer after all `multipart/byteranges` body parts. const PART_TRAILER: &[u8] = b"\r\n--B--\r\n"; -/// Produces a single chunk of the body and the following state, for use in an `unfold` call. -/// -/// Alternates between portions of `part_headers` and their corresponding bodies, then the overall -/// trailer, then end the stream. -fn next_multipart_body_chunk( - state: usize, - ent: &dyn Entity, - ranges: &[Range], - part_headers: &mut [Vec], -) -> impl Future, usize)>> +impl futures_core::Stream for MultipartStream where - D: 'static + Send + Sync + Buf + From> + From<&'static [u8]>, - E: 'static + Send + Sync, + D: 'static + Buf + From> + From<&'static [u8]>, + E: 'static + From, { - let i = state >> 1; - let odd = (state & 1) == 1; - let body = if i == ranges.len() && odd { - return futures_util::future::ready(None); - } else if i == ranges.len() { - InnerBody::Once(Some(PART_TRAILER.into())) - } else if odd { - InnerBody::B(Pin::from(ent.get_range(ranges[i].clone()))) - } else { - let v = std::mem::take(&mut part_headers[i]); - InnerBody::Once(Some(v.into())) - }; - futures_util::future::ready(Some((body, state + 1))) + type Item = Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = Pin::into_inner(self); + loop { + if let Some(ref mut cur) = this.cur { + match cur.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(d))) => { + this.remaining -= crate::as_u64(d.remaining()); + return Poll::Ready(Some(Ok(d))); + } + Poll::Ready(Some(Err(e))) => { + // Fuse. + this.remaining = 0; + this.state = this.ranges.len() << 1 | 1; + return Poll::Ready(Some(Err(e))); + } + Poll::Ready(None) => { + this.cur = None; + this.state += 1; + } + Poll::Pending => return Poll::Pending, + } + } + + let i = this.state >> 1; + let odd = (this.state & 1) == 1; + if i == this.ranges.len() && odd { + debug_assert_eq!(this.remaining, 0); + return Poll::Ready(None); + } + if i == this.ranges.len() { + this.state += 1; + this.remaining -= crate::as_u64(PART_TRAILER.len()); + return Poll::Ready(Some(Ok(PART_TRAILER.into()))); + } else if odd { + let r = &this.ranges[i]; + this.cur = Some(crate::body::ExactLenStream::new( + r.end - r.start, + this.entity.get_range(r.clone()), + )); + } else { + let v = std::mem::take(&mut this.part_headers[i]); + this.state += 1; + this.remaining -= crate::as_u64(v.len()); + return Poll::Ready(Some(Ok(v.into()))); + }; + } + } } diff --git a/tests/chunked-acceptance.rs b/tests/chunked-acceptance.rs index 8b86c5f..eb4f5be 100644 --- a/tests/chunked-acceptance.rs +++ b/tests/chunked-acceptance.rs @@ -6,27 +6,32 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; -use futures_util::{future, StreamExt}; +use bytes::Bytes; +use hyper_util::rt::TokioIo; use once_cell::sync::Lazy; use std::collections::HashMap; use std::io::{self, Write}; use std::sync::Mutex; +use tokio::net::TcpListener; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; -type BoxedError = Box; +type BoxError = Box; -async fn serve(req: http::Request) -> Result, BoxedError> { - let cmds = CMDS.lock().unwrap().remove(req.uri().path()).unwrap(); +async fn serve( + req: http::Request, +) -> Result>, BoxError> { + let mut cmds = CMDS.lock().unwrap().remove(req.uri().path()).unwrap(); let (resp, w) = http_serve::streaming_body(&req).build(); let mut w = w.unwrap(); - tokio::spawn(cmds.for_each(move |cmd| { - match cmd { - Cmd::WriteAll(s) => w.write_all(s).unwrap(), - Cmd::Abort(e) => w.abort(e), - Cmd::Flush => w.flush().unwrap(), + tokio::spawn(async move { + while let Some(cmd) = cmds.recv().await { + match cmd { + Cmd::WriteAll(s) => w.write_all(s).unwrap(), + Cmd::Abort(e) => w.abort(e), + Cmd::Flush => w.flush().unwrap(), + } } - future::ready(()) - })); + }); Ok(resp) } @@ -44,21 +49,28 @@ struct Server { fn new_server() -> Server { let (server_tx, server_rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let make_svc = hyper::service::make_service_fn(|_conn| { - future::ok::<_, hyper::Error>(hyper::service::service_fn(serve)) - }); let rt = tokio::runtime::Runtime::new().unwrap(); let _guard = rt.enter(); - - let addr = ([127, 0, 0, 1], 0).into(); - let srv = hyper::Server::bind(&addr).serve(make_svc); - let addr = srv.local_addr(); - server_tx - .send(Server { - addr: format!("http://{}:{}", addr.ip(), addr.port()), - }) - .unwrap(); - rt.block_on(srv).unwrap(); + rt.block_on(async { + let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + let addr = listener.local_addr().unwrap(); + server_tx + .send(Server { + addr: format!("http://{}:{}", addr.ip(), addr.port()), + }) + .unwrap(); + loop { + let (tcp, _) = listener.accept().await.unwrap(); + let io = TokioIo::new(tcp); + tokio::task::spawn(async move { + hyper::server::conn::http1::Builder::new() + .serve_connection(io, hyper::service::service_fn(serve)) + .await + .unwrap(); + }); + } + }); }); server_rx.recv().unwrap() } @@ -71,7 +83,7 @@ fn setup_req( path: &'static str, auto_gzip: bool, ) -> (UnboundedSender, reqwest::RequestBuilder) { - let (tx, rx) = mpsc::unbounded(); + let (tx, rx) = mpsc::unbounded_channel(); CMDS.lock().unwrap().insert(path, rx); let client = reqwest::Client::builder().gzip(auto_gzip).build().unwrap(); let req = client.get(&format!("{}{}", SERVER.addr, path)); @@ -82,8 +94,8 @@ async fn basic(path: &'static str, auto_gzip: bool) { let (cmds, req) = setup_req(path, auto_gzip); let resp = req.send().await.unwrap(); - cmds.unbounded_send(Cmd::WriteAll(b"1234")).unwrap(); - cmds.unbounded_send(Cmd::WriteAll(b"5678")).unwrap(); + cmds.send(Cmd::WriteAll(b"1234")).unwrap(); + cmds.send(Cmd::WriteAll(b"5678")).unwrap(); drop(cmds); assert_eq!(None, resp.headers().get(reqwest::header::CONTENT_ENCODING)); let buf = resp.bytes().await.unwrap(); @@ -104,13 +116,13 @@ async fn abort(path: &'static str, auto_gzip: bool) { let (cmds, req) = setup_req(path, auto_gzip); let mut resp = req.send().await.unwrap(); - cmds.unbounded_send(Cmd::WriteAll(b"1234")).unwrap(); - cmds.unbounded_send(Cmd::Flush).unwrap(); - cmds.unbounded_send(Cmd::WriteAll(b"5678")).unwrap(); + cmds.send(Cmd::WriteAll(b"1234")).unwrap(); + cmds.send(Cmd::Flush).unwrap(); + cmds.send(Cmd::WriteAll(b"5678")).unwrap(); let buf = resp.chunk().await.unwrap().unwrap(); assert_eq!(b"1234", &buf[..]); - cmds.unbounded_send(Cmd::Abort(Box::new(io::Error::new( + cmds.send(Cmd::Abort(Box::new(io::Error::new( io::ErrorKind::PermissionDenied, // note: not related to error kind below. "foo", )))) @@ -137,7 +149,7 @@ async fn manual_gzip() { let (cmds, req) = setup_req("/manual_gzip", false); let resp = req.header("Accept-Encoding", "gzip").send().await.unwrap(); - cmds.unbounded_send(Cmd::WriteAll(b"1234")).unwrap(); + cmds.send(Cmd::WriteAll(b"1234")).unwrap(); drop(cmds); assert_eq!( resp.headers().get(header::CONTENT_ENCODING).unwrap(), diff --git a/tests/entity-acceptance.rs b/tests/entity-acceptance.rs index eab01b0..55b190b 100644 --- a/tests/entity-acceptance.rs +++ b/tests/entity-acceptance.rs @@ -6,16 +6,20 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +use bytes::Bytes; use futures_core::Stream; use futures_util::{future, stream}; use http::header::HeaderValue; use http::{Request, Response}; -use hyper::body::Body; +use http_serve::Body; +use hyper_util::rt::TokioIo; use once_cell::sync::Lazy; use std::ops::Range; +use std::pin::Pin; use std::time::SystemTime; +use tokio::net::TcpListener; -type BoxedError = Box; +type BoxError = Box; static BODY: &'static [u8] = b"01234567890123456789012345678901234567890123456789012345678901234567890123456789\ @@ -37,8 +41,8 @@ impl http_serve::Entity for &'static FakeEntity { fn get_range( &self, range: Range, - ) -> Box> + Send + Sync> { - Box::new(stream::once(future::ok( + ) -> Pin> + Send + Sync>> { + Box::pin(stream::once(future::ok( BODY[range.start as usize..range.end as usize].into(), ))) } @@ -56,7 +60,9 @@ impl http_serve::Entity for &'static FakeEntity { } } -async fn serve(req: Request) -> Result, BoxedError> { +async fn serve( + req: Request, +) -> Result>, BoxError> { let entity: &'static FakeEntity = match req.uri().path() { "/none" => &*ENTITY_NO_ETAG, "/strong" => &*ENTITY_STRONG_ETAG, @@ -69,16 +75,24 @@ async fn serve(req: Request) -> Result, BoxedError> { fn new_server() -> String { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let make_svc = hyper::service::make_service_fn(|_conn| { - future::ok::<_, hyper::Error>(hyper::service::service_fn(serve)) - }); let rt = tokio::runtime::Runtime::new().unwrap(); let _guard = rt.enter(); - - let addr = ([127, 0, 0, 1], 0).into(); - let srv = hyper::Server::bind(&addr).serve(make_svc); - tx.send(srv.local_addr()).unwrap(); - rt.block_on(srv).unwrap(); + rt.block_on(async { + let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, 0)); + let listener = TcpListener::bind(addr).await.unwrap(); + let addr = listener.local_addr().unwrap(); + tx.send(addr).unwrap(); + loop { + let (tcp, _) = listener.accept().await.unwrap(); + let io = TokioIo::new(tcp); + tokio::task::spawn(async move { + hyper::server::conn::http1::Builder::new() + .serve_connection(io, hyper::service::service_fn(serve)) + .await + .unwrap(); + }); + } + }); }); let addr = rx.recv().unwrap(); format!("http://{}:{}", addr.ip(), addr.port()) @@ -116,6 +130,7 @@ async fn serve_without_etag() { resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); + assert!(resp.headers().get(reqwest::header::CONTENT_LENGTH).is_some()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); let buf = resp.bytes().await.unwrap(); assert_eq!(BODY, &buf[..]); @@ -132,6 +147,7 @@ async fn serve_without_etag() { resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), MIME ); + assert!(resp.headers().get(reqwest::header::CONTENT_LENGTH).is_some()); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); let buf = resp.bytes().await.unwrap(); assert_eq!(BODY, &buf[..]); @@ -193,6 +209,7 @@ async fn serve_without_etag() { .await .unwrap(); assert_eq!(reqwest::StatusCode::PARTIAL_CONTENT, resp.status()); + assert!(resp.headers().get(reqwest::header::CONTENT_LENGTH).is_some()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_RANGE).unwrap(), &format!("bytes 1-3/{}", BODY.len()) @@ -209,6 +226,7 @@ async fn serve_without_etag() { .unwrap(); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(reqwest::StatusCode::PARTIAL_CONTENT, resp.status()); + assert!(resp.headers().get(reqwest::header::CONTENT_LENGTH).is_some()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), &"multipart/byteranges; boundary=B" @@ -238,6 +256,7 @@ async fn serve_without_etag() { .await .unwrap(); assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); + assert!(resp.headers().get(reqwest::header::CONTENT_LENGTH).is_some()); assert_eq!(reqwest::StatusCode::OK, resp.status()); assert_eq!( resp.headers().get(reqwest::header::CONTENT_TYPE).unwrap(), @@ -490,3 +509,6 @@ async fn serve_with_weak_etag() { assert_eq!(resp.headers().get(reqwest::header::CONTENT_RANGE), None); assert_eq!(BODY, &resp.bytes().await.unwrap()[..]); } + +// TODO: stream that returns too much data. +// TODO: stream that returns too little data.