From 78b9656fb71a1499643730919e7f544e84e92a54 Mon Sep 17 00:00:00 2001 From: csd113 Date: Tue, 14 Apr 2026 11:53:26 -0700 Subject: [PATCH 1/4] Bump Arti to 0.41.0 and refresh docs --- Cargo.lock | 694 ++++++++++++++--------------------- Cargo.toml | 8 +- docs/arti-migration-guide.md | 36 +- 3 files changed, 296 insertions(+), 442 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7290208..3e40bbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,15 +44,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "alloca" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" -dependencies = [ - "cc", -] - [[package]] name = "amplify" version = "4.9.0" @@ -108,18 +99,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - [[package]] name = "anyhow" version = "1.0.102" @@ -134,9 +113,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arti-client" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89842cae6e3bda0fd128a5c66eb3392ed412065dc698c77d9fcc4b77e4159f2" +checksum = "5e15d2051582670d5c003deda168da03f0d3475f6375bca57d6b9852a9d32eed" dependencies = [ "async-trait", "cfg-if", @@ -155,6 +134,7 @@ dependencies = [ "rand 0.9.2", "safelog", "serde", + "tempfile", "thiserror 2.0.18", "time", "tor-async-utils", @@ -182,6 +162,7 @@ dependencies = [ "tor-rtcompat", "tracing", "void", + "web-time-compat", ] [[package]] @@ -594,12 +575,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beae2cb9f60bc3f21effaaf9c64e51f6627edd54eedc9199ba07f519ef2a2101" -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cc" version = "1.2.58" @@ -632,33 +607,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "cipher" version = "0.4.4" @@ -670,31 +618,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clap" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstyle", - "clap_lex", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - [[package]] name = "cmake" version = "0.1.58" @@ -790,70 +713,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "criterion" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" -dependencies = [ - "alloca", - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools 0.13.0", - "num-traits", - "oorandom", - "page_size", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-cycles-per-byte" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5396de42a52e9e5d8f67ef0702dae30451f310a9ba1c3094dcf228f0be0e54bc" -dependencies = [ - "cfg-if", - "criterion", -] - -[[package]] -name = "criterion-plot" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" -dependencies = [ - "cast", - "itertools 0.13.0", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -894,12 +753,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1160,13 +1013,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caef6056a5788d05d173cdc3c562ac28ae093828f851f69378b74e4e3d578e41" dependencies = [ "heck", - "indexmap 1.9.3", - "itertools 0.14.0", + "indexmap 2.13.0", + "itertools", "proc-macro-crate", "proc-macro2", "quote", "sha3", - "strum", + "strum 0.27.2", "syn 2.0.117", "void", ] @@ -1784,10 +1637,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -1831,17 +1686,6 @@ dependencies = [ "xxhash-rust", ] -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -2031,7 +1875,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -2157,6 +2001,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imara-diff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f01d462f766df78ab820dd06f5eb700233c51f0f4c2e846520eaf4ba6aa5c5c" +dependencies = [ + "hashbrown 0.15.5", + "memchr", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2218,15 +2072,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2635,12 +2480,6 @@ dependencies = [ "futures", ] -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "option-ext" version = "0.2.0" @@ -2703,16 +2542,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "parking" version = "2.2.1" @@ -2892,34 +2721,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "polling" version = "3.11.0" @@ -3151,26 +2952,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "rcgen" version = "0.13.2" @@ -3273,11 +3054,12 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "retry-error" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c322ea521636c5a3f13685a6266055b2dda7e54e2be35214d7c2a5d0672a5db" +checksum = "cf6aa271ee564cc5d1df57c5cf7c6ac7a21a4f9f40d2bf1d32bf0a1bb3ddaeb0" dependencies = [ "humantime", + "web-time", ] [[package]] @@ -3517,9 +3299,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "safelog" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8949ab2810bf603caef654634e5b4cedcbc05c120342a177cf8aaa122ef4bb76" +checksum = "ee9f10dd250956c65d58a19507dd06ff976f898560fe843580d05134541f0898" dependencies = [ "derive_more", "educe", @@ -3686,9 +3468,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -3909,20 +3691,20 @@ dependencies = [ ] [[package]] -name = "ssh-cipher" +name = "ssh-cipher-fork-arti" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +checksum = "125c5795103fc93fced42d123c8044180afc55469caa1ab56487c3c5543c898d" dependencies = [ "cipher", - "ssh-encoding", + "ssh-encoding-fork-arti", ] [[package]] -name = "ssh-encoding" +name = "ssh-encoding-fork-arti" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +checksum = "e0cf03c3a7ea88451ff83a129a79451fd9891d44fc76c25e916a11848b81814c" dependencies = [ "base64ct", "pem-rfc7468", @@ -3930,10 +3712,10 @@ dependencies = [ ] [[package]] -name = "ssh-key" +name = "ssh-key-fork-arti" version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" +checksum = "433782176b73ea7907763dc314c4a17438a231864d4aa683ba47c07c1cf0a388" dependencies = [ "num-bigint-dig", "p256", @@ -3944,8 +3726,8 @@ dependencies = [ "sec1", "sha2", "signature", - "ssh-cipher", - "ssh-encoding", + "ssh-cipher-fork-arti", + "ssh-encoding-fork-arti", "subtle", "zeroize", ] @@ -3980,7 +3762,16 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -3995,6 +3786,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4042,9 +3845,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.36.1" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" dependencies = [ "libc", "memchr", @@ -4165,16 +3968,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tls_codec" version = "0.4.2" @@ -4261,17 +4054,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap 2.13.0", "serde_core", - "serde_spanned 1.1.0", - "toml_datetime 0.7.5+spec-1.1.0", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.15", + "winnow 1.0.1", ] [[package]] @@ -4285,18 +4078,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.1.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -4322,16 +4106,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" dependencies = [ "indexmap 2.13.0", - "toml_datetime 1.1.0+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.1", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow 1.0.1", ] @@ -4344,15 +4128,15 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tor-async-utils" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895c61a46909134501c6815eceeb66c9c80fc494ee89429821b0f05ccf34b4f5" +checksum = "c4680d5ecdb052e950b31cd7c7852aa025968f648046e55251446d7d0e58138e" dependencies = [ "derive-deftly", "educe", @@ -4366,13 +4150,14 @@ dependencies = [ [[package]] name = "tor-basic-utils" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac6e4d7e131b7d69bc85558383cd4ac61e46b4dd0d4ed51632f28fac98cac0c" +checksum = "1a2351dee62e4edabb624a7e7ba85f6f2b73073ec4590e76c9b6f90fc30b91e9" dependencies = [ "derive_more", + "getrandom 0.3.4", "hex", - "itertools 0.14.0", + "itertools", "libc", "paste", "rand 0.9.2", @@ -4382,13 +4167,14 @@ dependencies = [ "smallvec", "thiserror 2.0.18", "weak-table", + "web-time-compat", ] [[package]] name = "tor-bytes" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64454947258e49f238a5f06a06250a0c54598a1c7409898b5c79505e6a99e7af" +checksum = "e86f91871fcd2e2fb16fcf55b7821b050b11d05d42b7e98104b662417bafda92" dependencies = [ "bytes", "derive-deftly", @@ -4404,9 +4190,9 @@ dependencies = [ [[package]] name = "tor-cell" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab0c79bc92a957e85959cf397a2d8f9c8294c35fa4f65247a9393b20ac95551" +checksum = "7ee932b1f0890a0ac689f6fdeec0fe4cc9e11bc1a18b2a09c2fb7adbd0761df4" dependencies = [ "amplify", "bitflags 2.11.0", @@ -4415,7 +4201,7 @@ dependencies = [ "derive-deftly", "derive_more", "educe", - "itertools 0.14.0", + "itertools", "paste", "rand 0.9.2", "smallvec", @@ -4435,9 +4221,9 @@ dependencies = [ [[package]] name = "tor-cert" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "debc911738298ee801fce4577c36a50c55295b0bb9c5519461b83cc486a1f86e" +checksum = "3f774e4097af30e759ddcfcdd50e594183b62c2e70e3dd55f4b61e254d527505" dependencies = [ "caret", "derive_builder_fork_arti", @@ -4448,21 +4234,24 @@ dependencies = [ "tor-checkable", "tor-error", "tor-llcrypto", + "web-time-compat", ] [[package]] name = "tor-chanmgr" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7af5b7c2f1e16d1304b5185fcbc91ca5c8df991c21be00702f925f055573eea1" +checksum = "31db74e069430a70f290820d7f60e2cd96364dcb4b8bbd92aee9bf94b9a2dfca" dependencies = [ "async-trait", + "base64ct", "caret", "cfg-if", "derive-deftly", "derive_more", "educe", "futures", + "httparse", "oneshot-fused-workaround", "percent-encoding", "postage", @@ -4488,25 +4277,27 @@ dependencies = [ "tracing", "url", "void", + "web-time-compat", ] [[package]] name = "tor-checkable" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b13a5b50bb55037f2e81b25dde42f420d57c75154216b8ef989006cea3ebee" +checksum = "d65310bcfd125f65e4ac8ee290b352d20a80909ef7226741f57e9a049391d3d7" dependencies = [ "humantime", "signature", "thiserror 2.0.18", "tor-llcrypto", + "web-time-compat", ] [[package]] name = "tor-circmgr" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b878f3f7c6be0c7f130d90b347ada2e7c46519dfbdde8273eae2e5d1792caa87" +checksum = "d1310bb4288894f3bae03d0201ee397bed95fd08b24b18b5303a0bdbee012fba" dependencies = [ "amplify", "async-trait", @@ -4519,7 +4310,7 @@ dependencies = [ "educe", "futures", "humantime-serde", - "itertools 0.14.0", + "itertools", "once_cell", "oneshot-fused-workaround", "pin-project", @@ -4549,13 +4340,14 @@ dependencies = [ "tracing", "void", "weak-table", + "web-time-compat", ] [[package]] name = "tor-config" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc74a00ab15bb986e3747c6969e40a58a63065d6f99077e7ee2f4657bb8b03" +checksum = "24a1148697cfeeb8c35a3bd2ee3eb98a3510610635fca63e46bca33dfb3f450e" dependencies = [ "amplify", "cfg-if", @@ -4567,7 +4359,7 @@ dependencies = [ "fs-mistrust", "futures", "humantime-serde", - "itertools 0.14.0", + "itertools", "notify", "paste", "postage", @@ -4575,9 +4367,9 @@ dependencies = [ "serde", "serde-value", "serde_ignored", - "strum", + "strum 0.28.0", "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "tor-basic-utils", "tor-error", "tor-rtcompat", @@ -4587,9 +4379,9 @@ dependencies = [ [[package]] name = "tor-config-path" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3005ab7b9a26a7271e5adf3dfb4ae18c09a943e32aeccc4f6d1c53a60de74b8d" +checksum = "2a3cc17b20f63fa231e3811005c3c39dd55b4c3bc44c73565be2a632cc53a442" dependencies = [ "directories", "serde", @@ -4601,21 +4393,26 @@ dependencies = [ [[package]] name = "tor-consdiff" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bfa2b7b71c72830f61c48da4bb3e13191e0c0e1404b9c5168c795e4f5feb4a8" +checksum = "77b5da8bcea62b5f6695f9271f8367d6d00977a6bffd2da9a0795c29cb839790" dependencies = [ + "derive_more", "digest", "hex", + "imara-diff", + "static_assertions", "thiserror 2.0.18", + "tor-error", "tor-llcrypto", + "tor-netdoc", ] [[package]] name = "tor-dirclient" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccd6fac844ac77c33ccdfcb56bf23ff40ebbb821ea708be79a481ec30e8c39c" +checksum = "139aec98a8579613a08b1283ca9da4ceda110e0627798bff22b1b692a7a03bf0" dependencies = [ "async-compression", "base64ct", @@ -4625,7 +4422,7 @@ dependencies = [ "http", "httparse", "httpdate", - "itertools 0.14.0", + "itertools", "memchr", "thiserror 2.0.18", "tor-circmgr", @@ -4637,13 +4434,14 @@ dependencies = [ "tor-proto", "tor-rtcompat", "tracing", + "web-time-compat", ] [[package]] name = "tor-dircommon" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cf39a3c30321d145a4d60753ae7ef5bb58a66a00ac9e2bfc30bd823faf2a4" +checksum = "4c0e029a636b59ac25e4c2e261e306775dac5200c1632f99696cfc2c236b3b5d" dependencies = [ "base64ct", "derive-deftly", @@ -4662,9 +4460,9 @@ dependencies = [ [[package]] name = "tor-dirmgr" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b52919aa9dbb82a354c5b904bef82e91beb702b9f8ad14e6eac4237d6128bf67" +checksum = "a659fc3d3aa9f1c11d940cd5fcc37a641a8db3595ec2b8df6dc4595e7d71ef08" dependencies = [ "async-trait", "base64ct", @@ -4679,7 +4477,7 @@ dependencies = [ "hex", "humantime", "humantime-serde", - "itertools 0.14.0", + "itertools", "memmap2", "oneshot-fused-workaround", "paste", @@ -4692,7 +4490,7 @@ dependencies = [ "serde_json", "signature", "static_assertions", - "strum", + "strum 0.28.0", "thiserror 2.0.18", "time", "tor-async-utils", @@ -4713,30 +4511,33 @@ dependencies = [ "tor-protover", "tor-rtcompat", "tracing", + "void", + "web-time-compat", ] [[package]] name = "tor-error" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595b005e6f571ac3890a34a00f361200aab781fd0218f2c528c86fc7af088df5" +checksum = "1f8163b225e07e00be618cbebdbbc8ff003795121971cdf54297e820cc177569" dependencies = [ "derive_more", "futures", "paste", "retry-error", "static_assertions", - "strum", + "strum 0.28.0", "thiserror 2.0.18", "tracing", "void", + "web-time-compat", ] [[package]] name = "tor-general-addr" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727b8c8bc01c1587486055edab5c2cd0d5c960f5bb3fac796fc9911872b8b397" +checksum = "b53d8066194461d13603437331c24c7a722d3d4e375ed1552c434b3240880f17" dependencies = [ "derive_more", "thiserror 2.0.18", @@ -4745,9 +4546,9 @@ dependencies = [ [[package]] name = "tor-guardmgr" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d337f465a477c0fb3b2faafa4654d70ff9df3590e57d22707591dddb4e4450c1" +checksum = "119df8b27549a1db143713b9afde6d0d5ef1736b9fd03264715bce9d83616d10" dependencies = [ "amplify", "base64ct", @@ -4759,7 +4560,7 @@ dependencies = [ "futures", "humantime", "humantime-serde", - "itertools 0.14.0", + "itertools", "num_enum", "oneshot-fused-workaround", "pin-project", @@ -4767,7 +4568,7 @@ dependencies = [ "rand 0.9.2", "safelog", "serde", - "strum", + "strum 0.28.0", "thiserror 2.0.18", "tor-async-utils", "tor-basic-utils", @@ -4784,13 +4585,14 @@ dependencies = [ "tor-rtcompat", "tor-units", "tracing", + "web-time-compat", ] [[package]] name = "tor-hsclient" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c701ef4eccab73f23d619ef5650c886239d1215e6d4aa0c3dec6342072a8178" +checksum = "9598bfa74aa4734be431d0538e4e6a39f34ab166a4f31ffb584a406b0c72d708" dependencies = [ "async-trait", "derive-deftly", @@ -4798,14 +4600,14 @@ dependencies = [ "educe", "either", "futures", - "itertools 0.14.0", + "itertools", "oneshot-fused-workaround", "postage", "rand 0.9.2", "retry-error", "safelog", "slotmap-careful", - "strum", + "strum 0.28.0", "thiserror 2.0.18", "tor-async-utils", "tor-basic-utils", @@ -4828,13 +4630,14 @@ dependencies = [ "tor-protover", "tor-rtcompat", "tracing", + "web-time-compat", ] [[package]] name = "tor-hscrypto" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3693cd43f05cd01ac0aaa060dae5c5e53c4364f89e0d769e33cd629a2fd3118" +checksum = "38f7f57feca263556c41c5d86129b6bd4b1f41dbfc566d070b112bd0bb1b6a21" dependencies = [ "cipher", "data-encoding", @@ -4843,7 +4646,7 @@ dependencies = [ "digest", "hex", "humantime", - "itertools 0.14.0", + "itertools", "paste", "rand 0.9.2", "safelog", @@ -4859,14 +4662,15 @@ dependencies = [ "tor-memquota", "tor-units", "void", + "web-time-compat", "zeroize", ] [[package]] name = "tor-hsservice" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2d6a533ffd01b6764bfa59526f12883d6025fd64fdc73e219033ec29936aa3" +checksum = "3cf88e0f0c67ccdd5de83e8e2011b7b39222197d6c98b500a11e8a2a85cf95f2" dependencies = [ "amplify", "async-trait", @@ -4882,7 +4686,7 @@ dependencies = [ "growable-bloom-filter", "hex", "humantime", - "itertools 0.14.0", + "itertools", "k12", "once_cell", "oneshot-fused-workaround", @@ -4893,7 +4697,7 @@ dependencies = [ "safelog", "serde", "serde_with", - "strum", + "strum 0.28.0", "thiserror 2.0.18", "tor-async-utils", "tor-basic-utils", @@ -4918,13 +4722,14 @@ dependencies = [ "tor-rtcompat", "tracing", "void", + "web-time-compat", ] [[package]] name = "tor-key-forge" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ade9065ae49cfe2ab020ca9ca9f2b3c5c9b5fc0d8980fa681d8b3a0668e042f" +checksum = "7cea1159867a98160a0629529fdb3615479d3f963b5ab92914dbcc6f0b68bf5c" dependencies = [ "derive-deftly", "derive_more", @@ -4933,7 +4738,7 @@ dependencies = [ "rand 0.9.2", "rsa", "signature", - "ssh-key", + "ssh-key-fork-arti", "thiserror 2.0.18", "tor-bytes", "tor-cert", @@ -4944,9 +4749,9 @@ dependencies = [ [[package]] name = "tor-keymgr" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243c3163d376c4723cd67271fcd6e5d6b498a6865c6b98299640e1be01c38826" +checksum = "98e14dbf1a37b9efb1df0650cdf5cb0d6da83589f6d42cfc4d2f7762d72594a5" dependencies = [ "amplify", "arrayvec", @@ -4960,12 +4765,12 @@ dependencies = [ "glob-match", "humantime", "inventory", - "itertools 0.14.0", + "itertools", "rand 0.9.2", "safelog", "serde", "signature", - "ssh-key", + "ssh-key-fork-arti", "thiserror 2.0.18", "tor-basic-utils", "tor-bytes", @@ -4979,14 +4784,15 @@ dependencies = [ "tracing", "visibility", "walkdir", + "web-time-compat", "zeroize", ] [[package]] name = "tor-linkspec" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f1ea8786900d6fbe4c9f775d341b1ba01bbd1f750d89bd63be78b6b01e1836" +checksum = "af62be67033640f5553797541720e34c12bf9ddb8569655d6241f32e7deed88f" dependencies = [ "base64ct", "by_address", @@ -4995,11 +4801,11 @@ dependencies = [ "derive_builder_fork_arti", "derive_more", "hex", - "itertools 0.14.0", + "itertools", "safelog", "serde", "serde_with", - "strum", + "strum 0.28.0", "thiserror 2.0.18", "tor-basic-utils", "tor-bytes", @@ -5011,9 +4817,9 @@ dependencies = [ [[package]] name = "tor-llcrypto" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6989a1c6d06ffd6835e2917edaae4aeef544f8e5fdd68b54cc365f2af523de" +checksum = "cc87d3def186b8edc21ec8f3645dd28cf3b13c6259f4e335e71b7ef52c6ea3f2" dependencies = [ "aes", "base64ct", @@ -5025,6 +4831,8 @@ dependencies = [ "digest", "ed25519-dalek", "educe", + "getrandom 0.2.17", + "getrandom 0.3.4", "getrandom 0.4.2", "hex", "rand 0.9.2", @@ -5051,9 +4859,9 @@ dependencies = [ [[package]] name = "tor-log-ratelim" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1cd642180923d12e3fab5996b4aa2189718da7f465df6eb196ce2b9c70e293" +checksum = "70066c9cf52f6c273243e97bc548c9be6e1d15e1bf5962da4f0be19686c85ade" dependencies = [ "futures", "humantime", @@ -5062,13 +4870,14 @@ dependencies = [ "tor-rtcompat", "tracing", "weak-table", + "web-time-compat", ] [[package]] name = "tor-memquota" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "599daea60fd3272eb72a795d1c593b45bbe15343cbc702340a81db124c06eed5" +checksum = "aeeba11704612b6fb277d708d0ff9b471266084d86a6e557f858cdadaa61ca49" dependencies = [ "cfg-if", "derive-deftly", @@ -5076,7 +4885,7 @@ dependencies = [ "dyn-clone", "educe", "futures", - "itertools 0.14.0", + "itertools", "paste", "pin-project", "serde", @@ -5097,21 +4906,21 @@ dependencies = [ [[package]] name = "tor-memquota-cost" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd92b07c0fc24e6d8166a5ff45e5b8654e68d89743c46d01889a16ab74c0b578" +checksum = "0178ecdfe9a2856232aaa41805dfd75b8512a21e8e6d02a67427d00e221b192b" dependencies = [ "derive-deftly", - "itertools 0.14.0", + "itertools", "paste", "void", ] [[package]] name = "tor-netdir" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41be8f47f521fc95206d2ba5facac8fb1a5b5b82169bd41ebeecdf46d1e77246" +checksum = "570b4d45feb00f9a04b429ba1a612b956b263ca01bb7b56a38e01d1223d581b7" dependencies = [ "async-trait", "bitflags 2.11.0", @@ -5120,11 +4929,11 @@ dependencies = [ "futures", "hex", "humantime", - "itertools 0.14.0", + "itertools", "num_enum", "rand 0.9.2", "serde", - "strum", + "strum 0.28.0", "thiserror 2.0.18", "time", "tor-basic-utils", @@ -5137,13 +4946,14 @@ dependencies = [ "tor-units", "tracing", "typed-index-collections", + "web-time-compat", ] [[package]] name = "tor-netdoc" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8bce73d2c78bd78a2a927336ca639cf6bd5d8ad092ebcd0b3fdeaa47dcc77e" +checksum = "ee1e809fc7d54cab30451fcb5f765ebb480ac8ad3e62454001ae875d36d1c27e" dependencies = [ "amplify", "base64ct", @@ -5156,7 +4966,7 @@ dependencies = [ "enumset", "hex", "humantime", - "itertools 0.14.0", + "itertools", "memchr", "paste", "phf", @@ -5166,7 +4976,7 @@ dependencies = [ "serde_with", "signature", "smallvec", - "strum", + "strum 0.28.0", "subtle", "thiserror 2.0.18", "time", @@ -5183,14 +4993,15 @@ dependencies = [ "tor-protover", "tor-units", "void", + "web-time-compat", "zeroize", ] [[package]] name = "tor-persist" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507ab4b6a3d59ed0df5804eeed66dcacde75e3be13d3694216cdfdb666bce625" +checksum = "6b6d2a9f700c0cbea6dd9b6b3614c47d26fbeffcb0ffdc10caf713a465fb913b" dependencies = [ "amplify", "derive-deftly", @@ -5200,7 +5011,7 @@ dependencies = [ "fslock", "fslock-guard", "futures", - "itertools 0.14.0", + "itertools", "oneshot-fused-workaround", "paste", "sanitize-filename", @@ -5213,13 +5024,14 @@ dependencies = [ "tor-error", "tracing", "void", + "web-time-compat", ] [[package]] name = "tor-proto" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfc552d535d36539d5782bb02028590bc472d219e49da51a96810725e80ff56" +checksum = "db8556b34a5a24a8ce20cd439ad2081b6097645e165d95387ba8eb8fa17f3520" dependencies = [ "amplify", "async-trait", @@ -5230,7 +5042,6 @@ dependencies = [ "cfg-if", "cipher", "coarsetime", - "criterion-cycles-per-byte", "derive-deftly", "derive_builder_fork_arti", "derive_more", @@ -5241,7 +5052,7 @@ dependencies = [ "futures-util", "hkdf", "hmac", - "itertools 0.14.0", + "itertools", "nonany", "oneshot-fused-workaround", "pin-project", @@ -5279,14 +5090,15 @@ dependencies = [ "typenum", "visibility", "void", + "web-time-compat", "zeroize", ] [[package]] name = "tor-protover" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aed88527d070c4b7ea4e55a36d2d56d0500e30ca66298b5264f047f7f2f89cfa" +checksum = "3fb1b457a24d4e6f4ab1981f33a55c7c525c2edcad855cbc4bdc544430914fc1" dependencies = [ "caret", "paste", @@ -5298,9 +5110,9 @@ dependencies = [ [[package]] name = "tor-relay-crypto" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e57e9f71b22ae1df63dbccc8e428cb07feec0abd654735109fa563c10bbb90" +checksum = "7675f5d412acbdf62f8771553d3ca0cfad8afdd78f9b1f7c2befc370ce030bda" dependencies = [ "derive-deftly", "derive_more", @@ -5312,13 +5124,14 @@ dependencies = [ "tor-keymgr", "tor-llcrypto", "tor-persist", + "web-time-compat", ] [[package]] name = "tor-relay-selection" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a372072ac9dea7d17e49693cc3f3ae77b3abf8125630516c9f2d622239b1920a" +checksum = "3110742f2a9e071c7f6c9278cd37e0bae0995d7171516fe4efb2de675645667e" dependencies = [ "rand 0.9.2", "serde", @@ -5330,9 +5143,9 @@ dependencies = [ [[package]] name = "tor-rtcompat" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14428b930e59003e801c0c32697c0aeb9b0495ad33ecbe8c6753bdb596233270" +checksum = "1714889530e08014a32123bcf947d9564dda61508bc4e7ec5203dcd726985850" dependencies = [ "async-trait", "async_executors", @@ -5359,14 +5172,15 @@ dependencies = [ "tor-general-addr", "tracing", "void", + "web-time-compat", "zeroize", ] [[package]] name = "tor-rtmock" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2da91a432cdaee8a93e0bb21b02f3e9c7667832ccbb4b54e00d9c1214638e70" +checksum = "4cc3bc02ab55161c3bb25b2ae64ea0e7a20b93cd3272f905dcbd8d5882d7a0be" dependencies = [ "amplify", "assert_matches", @@ -5376,12 +5190,12 @@ dependencies = [ "educe", "futures", "humantime", - "itertools 0.14.0", + "itertools", "oneshot-fused-workaround", "pin-project", "priority-queue", "slotmap-careful", - "strum", + "strum 0.28.0", "thiserror 2.0.18", "tor-error", "tor-general-addr", @@ -5389,13 +5203,14 @@ dependencies = [ "tracing", "tracing-test", "void", + "web-time-compat", ] [[package]] name = "tor-socksproto" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adbc9115a2f506d9bb86ae4446f0ca70eb523dc2f5e900a33582e7c39decc23a" +checksum = "36d84b4d4a8680dc865c7a7c44ea5701be049b88276b818f7cd6529af28c2115" dependencies = [ "amplify", "caret", @@ -5410,9 +5225,9 @@ dependencies = [ [[package]] name = "tor-units" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da90e93b4b4aa4ec356ecbe9e19aced36fdd655e94ca459d1915120d873363f0" +checksum = "2ae5c12f3bba5c4707f3a0a9c508320d6a4827e01e5e7ca0824ae55cbd2a737a" dependencies = [ "derive-deftly", "derive_more", @@ -5740,15 +5555,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" [[package]] -name = "web-sys" -version = "0.3.92" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "web-time-compat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39819265f219f60a92312f2755262dba9fff180a4ec281556863d69fa36adc59" +dependencies = [ + "web-time", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -5800,24 +5624,23 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.62.2", "windows-future", - "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -5829,18 +5652,31 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] name = "windows-future" -version = "0.2.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core", - "windows-link 0.1.3", + "windows-core 0.62.2", + "windows-link 0.2.1", "windows-threading", ] @@ -5880,12 +5716,12 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core", - "windows-link 0.1.3", + "windows-core 0.62.2", + "windows-link 0.2.1", ] [[package]] @@ -5897,6 +5733,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -5906,6 +5751,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5977,11 +5831,11 @@ dependencies = [ [[package]] name = "windows-threading" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4937e81..ba3dd2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "rusthost" version = "1.3.1" edition = "2021" # Bumped from 1.86 → 1.90 to match the highest MSRV in the transitive dep tree: -# arti-client 0.40 and most tor-* crates require 1.89 +# arti-client 0.41 and most tor-* crates require 1.90 # typed-index-collections 3.5 (via tor-netdir) requires 1.90 rust-version = "1.90" description = "Single-binary, zero-setup static site hosting appliance with Tor support" @@ -62,13 +62,13 @@ tokio = { version = "1", features = [ # default-features = false removes the implicit "native-tls" default that # would otherwise pull in openssl-sys and break cross-compilation. -arti-client = { version = "0.40", default-features = false, features = [ +arti-client = { version = "0.41", default-features = false, features = [ "tokio", "rustls", "onion-service-service", ] } -tor-hsservice = { version = "0.40", default-features = false } -tor-cell = { version = "0.40", default-features = false } +tor-hsservice = { version = "0.41", default-features = false } +tor-cell = { version = "0.41", default-features = false } futures = "0.3" # Terminal auto-launcher (Phase 0): TTY detection uses std::io::IsTerminal diff --git a/docs/arti-migration-guide.md b/docs/arti-migration-guide.md index 83ba213..8b2f972 100644 --- a/docs/arti-migration-guide.md +++ b/docs/arti-migration-guide.md @@ -5,8 +5,8 @@ spawning the system C Tor binary as a subprocess to running Arti, the official Tor Project Rust implementation, fully in-process. Use this as a step-by-step reference for applying the same migration to other projects. -> **Tested against:** `arti-client 0.40`, `tor-hsservice 0.40`, `tor-cell 0.40` -> on Rust 1.86 (macOS arm64 + x86, Linux x86_64). +> **Tested against:** `arti-client 0.41.0`, `tor-hsservice 0.41.0`, `tor-cell 0.41.0` +> on Rust 1.90 (macOS arm64 + x86, Linux x86_64). --- @@ -52,15 +52,15 @@ Project and versioned together. ### MSRV -Arti 0.40+ requires Rust **1.86**. If your project targets 1.85 or lower, -bump it: +Arti 0.41+ requires Rust **1.90** in this codebase. If your project targets +1.89 or lower, bump it: ```toml # Before -rust-version = "1.85" +rust-version = "1.89" # After -rust-version = "1.86" +rust-version = "1.90" ``` ### Add dependencies @@ -70,27 +70,27 @@ Add these six entries to `[dependencies]`: ```toml # arti-client: high-level Tor client. Features needed for onion service hosting: # tokio — Tokio async runtime backend (required) -# native-tls — TLS for connecting to Tor relays (required) +# rustls — TLS for connecting to Tor relays (required) # onion-service-service — enables *hosting* onion services -arti-client = { version = "0.40", features = [ +arti-client = { version = "0.41", features = [ "tokio", - "native-tls", + "rustls", "onion-service-service", ] } # tor-hsservice: lower-level onion service types used directly: # OnionServiceConfigBuilder, handle_rend_requests, HsId, StreamRequest -tor-hsservice = { version = "0.40" } +tor-hsservice = { version = "0.41" } # tor-cell: needed to construct the Connected message passed to # StreamRequest::accept(Connected) — see the stream proxying section -tor-cell = { version = "0.40" } +tor-cell = { version = "0.41" } # futures: StreamExt::next() for iterating the stream of incoming connections futures = "0.3" # sha3 + data-encoding: used to encode HsId → "${base32}.onion" manually. -# HsId does not implement std::fmt::Display in arti-client 0.40 — see the +# HsId does not implement std::fmt::Display in arti-client 0.41 — see the # "Getting the onion address" section for the full explanation. sha3 = "0.10" data-encoding = "2" @@ -139,7 +139,7 @@ use tor_hsservice::{config::OnionServiceConfigBuilder, handle_rend_requests, HsI Notes on the import changes: - `TorClientConfig` is **not** imported — it is not used directly. The builder is accessed via `TorClientConfigBuilder` instead (see the config section below). -- `HsId` is imported from `tor_hsservice`, **not** from `arti_client`. In arti 0.40, the re-export in `arti_client` is gated behind `feature = "onion-service-client"` and `feature = "experimental-api"` — neither of which is enabled in this setup. `tor_hsservice::HsId` is the ungated path. +- `HsId` is imported from `tor_hsservice`, **not** from `arti_client`. In arti 0.41, the re-export in `arti_client` is gated behind `feature = "onion-service-client"` and `feature = "experimental-api"` — neither of which is enabled in this setup. `tor_hsservice::HsId` is the ungated path. --- @@ -299,7 +299,7 @@ is no subprocess stderr to collect and no child process to kill on panic. **Pitfall 1:** `onion_name()` is deprecated. Use `onion_address()` instead. **Pitfall 2:** `HsId` does **not** implement `std::fmt::Display` in -`arti-client 0.40`. Neither `format!("{}", hsid)` nor `.to_string()` compile, +`arti-client 0.41`. Neither `format!("{}", hsid)` nor `.to_string()` compile, regardless of whether you got the `HsId` from `onion_name()` or `onion_address()`: @@ -330,7 +330,7 @@ Import `HsId` from `tor_hsservice` instead — it is ungated there. // Wrong — deprecated let onion_name = onion_service.onion_name()?.to_string(); // ← deprecated + E0599 -// Wrong — onion_address() returns HsId, which still has no Display in 0.40 +// Wrong — onion_address() returns HsId, which still has no Display in 0.41 let onion_name = format!( "{}", onion_service.onion_address().ok_or("...")? // ← E0599 @@ -378,7 +378,7 @@ fn hsid_to_onion_address(hsid: HsId) -> String { ``` This implements the [v3 onion address spec](https://spec.torproject.org/rend-spec/overview.html) -directly. `HsId: AsRef<[u8; 32]>` is stable across arti 0.40+, so it will +directly. `HsId: AsRef<[u8; 32]>` is stable across arti 0.41+, so it will keep working regardless of whether `Display` is ever added to `HsId`. The address is available immediately when `launch_onion_service` returns — @@ -641,7 +641,7 @@ async fn set_onion(state: &SharedState, addr: String) { | File | Change | |---|---| -| `Cargo.toml` | `rust-version` 1.85 → 1.86; add `arti-client`, `tor-hsservice`, `tor-cell`, `futures`, `sha3`, `data-encoding` | +| `Cargo.toml` | `rust-version` 1.89 → 1.90; add `arti-client`, `tor-hsservice`, `tor-cell`, `futures`, `sha3`, `data-encoding` | | `src/tor/mod.rs` | Complete rewrite (see above) | | `src/tor/torrc.rs` | Delete | | `src/config/defaults.rs` | Update `[tor]` comment block | @@ -664,7 +664,7 @@ exact fix for each one. | `E0277: CfgPath: From` | Used `.storage().cache_dir(PathBuf)` | Use `TorClientConfigBuilder::from_directories(state, cache)` | | `E0599: no method named 'from_directories'` | Called `TorClientConfig::builder().from_directories(…)` as a method chain | `from_directories` is an associated function — call it as `TorClientConfigBuilder::from_directories(…).build()?` directly | | `deprecated: onion_name` | Called `.onion_name()` | Use `.onion_address()` instead | -| `E0599: HsId doesn't implement Display` | Called `format!("{}", hsid)` or `.to_string()` on `HsId` | `HsId` has no `Display` in arti 0.40. Add `sha3 = "0.10"` and `data-encoding = "2"` deps and encode the address manually with `hsid_to_onion_address(hsid)` using `HsId: AsRef<[u8; 32]>` | +| `E0599: HsId doesn't implement Display` | Called `format!("{}", hsid)` or `.to_string()` on `HsId` | `HsId` has no `Display` in arti 0.41. Add `sha3 = "0.10"` and `data-encoding = "2"` deps and encode the address manually with `hsid_to_onion_address(hsid)` using `HsId: AsRef<[u8; 32]>` | | `E0425: cannot find type 'HsId' in crate 'arti_client'` | Wrote `arti_client::HsId` in function signature | `HsId` is feature-gated in `arti_client`. Import it from `tor_hsservice::HsId` instead — it is ungated there | | `E0061: accept() takes 1 argument` | Called `stream_req.accept()` with no args | Use `stream_req.accept(Connected::new_empty())` | | `warn(unused_imports): sync::Arc` | Pre-existing unused import unmasked | Remove `Arc` from the import line | From 77cae4c390d602d5138899c096c66843194f719f Mon Sep 17 00:00:00 2001 From: csd113 Date: Tue, 14 Apr 2026 15:16:59 -0700 Subject: [PATCH 2/4] Handle overload and serve-root edge cases --- src/runtime/lifecycle.rs | 17 +- src/server/mod.rs | 40 +- tests/fixtures/html_stress/assets/logo.svg | 5 + .../fixtures/html_stress/gallery/thumb-a.txt | 1 + .../fixtures/html_stress/gallery/thumb-b.txt | 1 + tests/fixtures/html_stress/index.html | 51 ++ tests/fixtures/html_stress/pages/about.html | 12 + tests/fixtures/html_stress/pages/huge.html | 11 + .../html_stress/pages/nested/index.html | 11 + .../html_stress/pages/space name.html | 11 + tests/fixtures/html_stress/scripts/app.js | 12 + tests/fixtures/html_stress/styles/app.css | 51 ++ tests/fixtures/runtime/logs/access.log | 244 +++++++++ tests/html_stress.rs | 471 ++++++++++++++++++ tests/http_integration.rs | 3 +- 15 files changed, 926 insertions(+), 15 deletions(-) create mode 100644 tests/fixtures/html_stress/assets/logo.svg create mode 100644 tests/fixtures/html_stress/gallery/thumb-a.txt create mode 100644 tests/fixtures/html_stress/gallery/thumb-b.txt create mode 100644 tests/fixtures/html_stress/index.html create mode 100644 tests/fixtures/html_stress/pages/about.html create mode 100644 tests/fixtures/html_stress/pages/huge.html create mode 100644 tests/fixtures/html_stress/pages/nested/index.html create mode 100644 tests/fixtures/html_stress/pages/space name.html create mode 100644 tests/fixtures/html_stress/scripts/app.js create mode 100644 tests/fixtures/html_stress/styles/app.css create mode 100644 tests/fixtures/runtime/logs/access.log create mode 100644 tests/html_stress.rs diff --git a/src/runtime/lifecycle.rs b/src/runtime/lifecycle.rs index 3ab5443..c96881f 100644 --- a/src/runtime/lifecycle.rs +++ b/src/runtime/lifecycle.rs @@ -117,14 +117,19 @@ async fn one_shot_serve(dir: PathBuf, port: u16, tor_enabled: bool, headless: bo }; use std::num::NonZeroU16; - let dir_str = dir.to_string_lossy().into_owned(); + let canonical_dir = dir.canonicalize().unwrap_or_else(|_| dir.clone()); + // Use "." when the served path has no leaf name (for example `/`), so the + // resulting `data_dir.join(site.directory)` still resolves back to `dir`. + let site_dir = canonical_dir + .file_name() + .and_then(|name| name.to_str()) + .map(str::to_owned) + .unwrap_or_else(|| ".".to_owned()); // Use the parent of `dir` as the data_dir so relative paths stay sane. - let data_dir = dir - .canonicalize() - .unwrap_or_else(|_| dir.clone()) + let data_dir = canonical_dir .parent() - .map_or_else(|| dir.clone(), Path::to_path_buf); + .map_or_else(|| canonical_dir.clone(), Path::to_path_buf); let config = Arc::new(crate::config::Config { server: ServerConfig { @@ -141,7 +146,7 @@ async fn one_shot_serve(dir: PathBuf, port: u16, tor_enabled: bool, headless: bo trusted_proxies: None, }, site: SiteConfig { - directory: dir_str, + directory: site_dir, index_file: "index.html".into(), enable_directory_listing: true, expose_dotfiles: false, diff --git a/src/server/mod.rs b/src/server/mod.rs index 23bde99..1b8d13e 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -33,6 +33,7 @@ use std::{ time::Duration, }; use tokio::{ + io::AsyncWriteExt as _, net::TcpListener, sync::{oneshot, watch, Semaphore}, task::JoinSet, @@ -104,6 +105,29 @@ fn try_acquire_per_ip( } } } + +const OVERLOAD_RESPONSE: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 0\r\n\r\n"; + +fn reject_overloaded_connection( + stream: tokio::net::TcpStream, + peer_ip: IpAddr, + label: &'static str, +) { + tokio::spawn(async move { + let mut stream = stream; + if let Err(e) = stream.write_all(OVERLOAD_RESPONSE).await { + log::debug!( + "Could not send overload response for {label} connection from {peer_ip}: {e}" + ); + return; + } + if let Err(e) = stream.shutdown().await { + log::debug!( + "Could not close overload response for {label} connection from {peer_ip}: {e}" + ); + } + }); +} // ─── Server context ─────────────────────────────────────────────────────────── /// Shared references prepared once before the accept loop starts. /// @@ -211,8 +235,8 @@ impl ServerContext { let peer_ip = peer.ip(); let ip_guard = if let Some(limit) = self.max_per_ip { let Ok(ip_guard) = try_acquire_per_ip(&self.per_ip_map, peer_ip, limit) else { - log::warn!("Per-IP limit ({limit}) reached for {peer_ip}; dropping connection"); - drop(stream); + log::warn!("Per-IP limit ({limit}) reached for {peer_ip}; sending 503 response"); + reject_overloaded_connection(stream, peer_ip, "HTTP"); return true; }; Some(ip_guard) @@ -221,10 +245,10 @@ impl ServerContext { }; let Ok(permit) = Arc::clone(&self.semaphore).try_acquire_owned() else { log::warn!( - "Connection limit ({}) reached; rejecting connection from {peer_ip}", + "Connection limit ({}) reached; sending 503 response to {peer_ip}", self.max_conns ); - drop(stream); + reject_overloaded_connection(stream, peer_ip, "HTTP"); return true; }; let site = Arc::clone(&self.canonical_root); @@ -490,9 +514,9 @@ pub async fn run_https( let ip_guard = if let Some(limit) = ctx.max_per_ip { let Ok(ip_guard) = try_acquire_per_ip(&ctx.per_ip_map, peer_ip, limit) else { log::warn!( - "Per-IP limit ({limit}) reached for {peer_ip}; dropping TLS connection" + "Per-IP limit ({limit}) reached for {peer_ip}; sending 503 response" ); - drop(tcp_stream); + reject_overloaded_connection(tcp_stream, peer_ip, "HTTPS"); continue; }; Some(ip_guard) @@ -501,10 +525,10 @@ pub async fn run_https( }; let Ok(permit) = Arc::clone(&ctx.semaphore).try_acquire_owned() else { log::warn!( - "Connection limit ({}) reached; rejecting TLS connection from {peer_ip}", + "Connection limit ({}) reached; sending 503 response to {peer_ip}", ctx.max_conns ); - drop(tcp_stream); + reject_overloaded_connection(tcp_stream, peer_ip, "HTTPS"); continue; }; let site = Arc::clone(&ctx.canonical_root); diff --git a/tests/fixtures/html_stress/assets/logo.svg b/tests/fixtures/html_stress/assets/logo.svg new file mode 100644 index 0000000..fb0b62c --- /dev/null +++ b/tests/fixtures/html_stress/assets/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/fixtures/html_stress/gallery/thumb-a.txt b/tests/fixtures/html_stress/gallery/thumb-a.txt new file mode 100644 index 0000000..bffb63e --- /dev/null +++ b/tests/fixtures/html_stress/gallery/thumb-a.txt @@ -0,0 +1 @@ +thumbnail-a diff --git a/tests/fixtures/html_stress/gallery/thumb-b.txt b/tests/fixtures/html_stress/gallery/thumb-b.txt new file mode 100644 index 0000000..1fecd8a --- /dev/null +++ b/tests/fixtures/html_stress/gallery/thumb-b.txt @@ -0,0 +1 @@ +thumbnail-b diff --git a/tests/fixtures/html_stress/index.html b/tests/fixtures/html_stress/index.html new file mode 100644 index 0000000..c3753c3 --- /dev/null +++ b/tests/fixtures/html_stress/index.html @@ -0,0 +1,51 @@ + + + + + + RustHost Stress Suite + + + + + +
+
+

RustHost Stress Suite

+

The app should serve this page, its linked assets, and nested pages without failing under bursty keep-alive traffic.

+
+ + + +
+
+

HTML

+

Root, nested, and spaced filenames.

+
+
+

CSS

+

One stylesheet with obvious markers.

+
+
+

JS

+

One defer-loaded script with a window flag.

+
+
+

Images

+

SVG asset for content-type coverage.

+
+
+ +
+ RustHost mark +
Static content with real browser-like references.
+
+
+ + diff --git a/tests/fixtures/html_stress/pages/about.html b/tests/fixtures/html_stress/pages/about.html new file mode 100644 index 0000000..8949e79 --- /dev/null +++ b/tests/fixtures/html_stress/pages/about.html @@ -0,0 +1,12 @@ + + + + + About + + +

About this fixture

+

This page exercises a normal nested HTML route.

+

Back to root

+ + diff --git a/tests/fixtures/html_stress/pages/huge.html b/tests/fixtures/html_stress/pages/huge.html new file mode 100644 index 0000000..b4f0cf3 --- /dev/null +++ b/tests/fixtures/html_stress/pages/huge.html @@ -0,0 +1,11 @@ + + + + + Huge page placeholder + + +

Huge page placeholder

+

This placeholder is replaced at test time with a much larger body so range requests and streaming get exercised.

+ + diff --git a/tests/fixtures/html_stress/pages/nested/index.html b/tests/fixtures/html_stress/pages/nested/index.html new file mode 100644 index 0000000..377b398 --- /dev/null +++ b/tests/fixtures/html_stress/pages/nested/index.html @@ -0,0 +1,11 @@ + + + + + Nested index + + +

Nested index page

+

This file checks that nested index lookup still works.

+ + diff --git a/tests/fixtures/html_stress/pages/space name.html b/tests/fixtures/html_stress/pages/space name.html new file mode 100644 index 0000000..9eb3903 --- /dev/null +++ b/tests/fixtures/html_stress/pages/space name.html @@ -0,0 +1,11 @@ + + + + + Space Name + + +

Filename with a space

+

The request path should decode %20 correctly.

+ + diff --git a/tests/fixtures/html_stress/scripts/app.js b/tests/fixtures/html_stress/scripts/app.js new file mode 100644 index 0000000..4e22874 --- /dev/null +++ b/tests/fixtures/html_stress/scripts/app.js @@ -0,0 +1,12 @@ +window.__rusthostStressSuite = { + ready: true, + title: "RustHost Stress Suite", + loadedAt: new Date().toISOString(), +}; + +document.addEventListener("DOMContentLoaded", () => { + const status = document.querySelector("#status"); + if (status) { + status.textContent = "The fixture script loaded successfully."; + } +}); diff --git a/tests/fixtures/html_stress/styles/app.css b/tests/fixtures/html_stress/styles/app.css new file mode 100644 index 0000000..b89bb99 --- /dev/null +++ b/tests/fixtures/html_stress/styles/app.css @@ -0,0 +1,51 @@ +:root { + color-scheme: light; + --bg: #f4efe8; + --fg: #1f2430; + --accent: #c18d2a; +} + +html, +body { + margin: 0; + min-height: 100%; + background: radial-gradient(circle at top, #fff7ec 0%, var(--bg) 55%, #e6dfd6 100%); + color: var(--fg); + font-family: Georgia, "Times New Roman", serif; +} + +.shell { + max-width: 64rem; + margin: 0 auto; + padding: 3rem 1.5rem 4rem; +} + +.hero h1 { + margin: 0 0 0.5rem; + font-size: clamp(2.5rem, 6vw, 4.5rem); + line-height: 1.05; +} + +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: 1rem; + margin-top: 2rem; +} + +.cards article, +.brand { + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(31, 36, 48, 0.12); + border-radius: 1rem; + box-shadow: 0 1rem 2.5rem rgba(31, 36, 48, 0.08); + padding: 1rem 1.1rem; +} + +a { + color: var(--accent); +} + +code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} diff --git a/tests/fixtures/runtime/logs/access.log b/tests/fixtures/runtime/logs/access.log new file mode 100644 index 0000000..7e12987 --- /dev/null +++ b/tests/fixtures/runtime/logs/access.log @@ -0,0 +1,244 @@ +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:25 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET / HTTP/1.1" 200 1673 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /scripts/app.js HTTP/1.1" 200 322 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /assets/logo.svg HTTP/1.1" 200 305 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /styles/app.css HTTP/1.1" 200 922 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /gallery/ HTTP/1.1" 200 463 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/nested/index.html HTTP/1.1" 200 219 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/space%20name.html HTTP/1.1" 200 225 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "HEAD /pages/about.html HTTP/1.1" 200 244 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" +127.0.0.1 - - [14/Apr/2026:15:05:41 -0700] "GET /pages/huge.html HTTP/1.1" 206 256 "-" "-" diff --git a/tests/html_stress.rs b/tests/html_stress.rs new file mode 100644 index 0000000..c9d7c07 --- /dev/null +++ b/tests/html_stress.rs @@ -0,0 +1,471 @@ +//! HTML stress-test suite for RustHost. +//! +//! This test serves the fixture tree under `tests/fixtures/html_stress` through +//! the real HTTP server, then hammers it with a mixed workload: +//! - repeated keep-alive requests on the same connection +//! - concurrent clients +//! - HTML, CSS, JS, SVG, directory listing, and percent-encoded paths +//! - a generated large HTML page with range requests + +use std::{ + fs, + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; + +use dashmap::DashMap; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + sync::{watch, RwLock, Semaphore}, +}; + +use rusthost::{ + config::Config, + runtime::state::{AppState, Metrics}, +}; + +fn reserve_port() -> Result { + let listener = + std::net::TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))?; + Ok(listener.local_addr()?.port()) +} + +fn fixture_site_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("html_stress") +} + +fn materialize_large_page(site_root: &Path) -> Result<(), Box> { + let large_path = site_root.join("pages/huge.html"); + let mut body = String::from( + "\n\n\n \n Huge page\n\n\n

Huge page

\n
\n",
+    );
+    for i in 0..4096 {
+        body.push_str(&format!("line {:04}: RustHost static stress body\n", i));
+    }
+    body.push_str("  
\n\n\n"); + fs::write(large_path, body)?; + Ok(()) +} + +fn copy_tree(src: &Path, dst: &Path) -> Result<(), Box> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + if ty.is_dir() { + copy_tree(&src_path, &dst_path)?; + } else if ty.is_file() { + fs::copy(&src_path, &dst_path)?; + } + } + Ok(()) +} + +fn build_site_copy() -> Result<(tempfile::TempDir, PathBuf), Box> { + let tmp = tempfile::tempdir()?; + let site = tmp.path().join("site"); + copy_tree(&fixture_site_root(), &site)?; + materialize_large_page(&site)?; + Ok((tmp, site)) +} + +fn build_test_config( + site_root: &Path, + bind_addr: IpAddr, + port: u16, +) -> Result> { + use std::num::NonZeroU16; + + let dir_name = site_root + .file_name() + .and_then(|n| n.to_str()) + .ok_or("site_root must have a valid UTF-8 directory name")? + .to_owned(); + + let mut config = Config::default(); + config.server.port = NonZeroU16::new(port).ok_or("reserved port cannot be zero")?; + config.server.bind = bind_addr; + config.server.auto_port_fallback = false; + config.server.open_browser_on_start = false; + config.server.max_connections = 128; + config.server.max_connections_per_ip = 128; + config.server.shutdown_grace_secs = 5; + config.site.directory = dir_name; + config.site.index_file = "index.html".into(); + config.site.enable_directory_listing = true; + config.site.spa_routing = false; + config.tor.enabled = false; + config.console.interactive = false; + Ok(config) +} + +fn response_to_str(raw: &[u8]) -> Result<&str, Box> { + std::str::from_utf8(raw) + .map_err(|e| format!("response contained non-UTF-8 bytes (error: {e}):\n{raw:?}").into()) +} + +fn status_code(raw: &[u8]) -> Result> { + let text = response_to_str(raw)?; + text.split_whitespace() + .nth(1) + .and_then(|s| s.parse().ok()) + .ok_or_else(|| format!("malformed status line in response:\n{text}").into()) +} + +fn header_value(raw: &[u8], name: &str) -> Result, Box> { + let needle = format!("{}:", name.to_ascii_lowercase()); + let text = response_to_str(raw)?; + Ok(text + .lines() + .skip(1) + .find(|l| l.to_ascii_lowercase().starts_with(&needle)) + .and_then(|l| l.split_once(':').map(|(_, v)| v.trim().to_owned()))) +} + +fn body_bytes(raw: &[u8]) -> Result<&[u8], Box> { + let text = response_to_str(raw)?; + let sep = text + .find("\r\n\r\n") + .ok_or("response missing header terminator")?; + Ok(raw + .get(sep + 4..) + .ok_or("response body slice out of bounds")?) +} + +fn find_header_end(buf: &[u8], search_from: usize) -> Option { + let tail = buf.get(search_from..)?; + let pos = tail.windows(4).position(|w| w == b"\r\n\r\n")?; + Some(search_from.saturating_add(pos).saturating_add(4)) +} + +async fn read_one_response(stream: &mut S) -> Result, Box> +where + S: AsyncRead + Unpin, +{ + let mut buf = Vec::with_capacity(4096); + let mut staging = [0u8; 4096]; + let header_end; + + loop { + let search_from = buf.len().saturating_sub(3); + let n = stream.read(&mut staging).await?; + if n == 0 { + return Err("connection closed before \\r\\n\\r\\n header terminator".into()); + } + buf.extend_from_slice( + staging + .get(..n) + .ok_or("read returned more bytes than the staging buffer can hold")?, + ); + if let Some(end) = find_header_end(&buf, search_from) { + header_end = end; + break; + } + } + + let header_bytes = buf.get(..header_end).ok_or("header_end is out of bounds")?; + let header_str = std::str::from_utf8(header_bytes) + .map_err(|e| format!("response headers are not valid UTF-8: {e}"))?; + + let status: u16 = header_str + .lines() + .next() + .and_then(|l| l.split_whitespace().nth(1)) + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + let content_length: usize = header_str + .lines() + .find(|l| l.to_ascii_lowercase().starts_with("content-length:")) + .and_then(|l| l.split_once(':').map(|x| x.1)) + .and_then(|v| v.trim().parse().ok()) + .unwrap_or(0); + + let has_body = content_length > 0 && !matches!(status, 204 | 304); + + if !has_body { + buf.truncate(header_end); + return Ok(buf); + } + + let already_have = buf.len().saturating_sub(header_end); + let total_needed = header_end + .checked_add(content_length) + .ok_or("Content-Length value causes usize overflow")?; + + if already_have < content_length { + buf.resize(total_needed, 0); + let fill_start = header_end + .checked_add(already_have) + .ok_or("fill_start overflows usize")?; + let body_slice = buf + .get_mut(fill_start..total_needed) + .ok_or("body slice range is out of bounds")?; + stream.read_exact(body_slice).await?; + } else { + buf.truncate(total_needed); + } + + Ok(buf) +} + +async fn read_headers_only(stream: &mut S) -> Result, Box> +where + S: AsyncRead + Unpin, +{ + let mut buf = Vec::with_capacity(4096); + let mut staging = [0u8; 4096]; + + loop { + let search_from = buf.len().saturating_sub(3); + let n = stream.read(&mut staging).await?; + if n == 0 { + return Err("connection closed before \\r\\n\\r\\n header terminator".into()); + } + buf.extend_from_slice( + staging + .get(..n) + .ok_or("read returned more bytes than the staging buffer can hold")?, + ); + if let Some(end) = find_header_end(&buf, search_from) { + buf.truncate(end); + return Ok(buf); + } + } +} + +async fn read_response( + stream: &mut TcpStream, + request: &[u8], +) -> Result, Box> { + stream.write_all(request).await?; + read_one_response(stream).await +} + +struct TestServer { + addr: SocketAddr, + shutdown_tx: watch::Sender, + _root_tx: watch::Sender>, + handle: Option>, +} + +impl TestServer { + async fn start(site_root: &Path) -> Result> { + let port = reserve_port()?; + let data_dir = site_root.parent().unwrap_or(site_root).to_path_buf(); + let config = Arc::new(build_test_config( + site_root, + IpAddr::V4(Ipv4Addr::LOCALHOST), + port, + )?); + let state = Arc::new(RwLock::new(AppState::new())); + let metrics = Arc::new(Metrics::new()); + let (shutdown_tx, shutdown_rx) = watch::channel(false); + let (port_tx, port_rx) = tokio::sync::oneshot::channel::(); + + let joined = data_dir.join(&config.site.directory); + let site_root_arc: Arc = Arc::from(joined.as_path()); + let (root_tx, root_rx) = watch::channel(site_root_arc); + + let conn_semaphore = Arc::new(Semaphore::new(config.server.max_connections as usize)); + let ip_connections = Arc::new(DashMap::new()); + + let handle = { + let cfg = Arc::clone(&config); + let st = Arc::clone(&state); + let met = Arc::clone(&metrics); + tokio::spawn(async move { + rusthost::server::run( + cfg, + st, + met, + data_dir, + shutdown_rx, + port_tx, + root_rx, + conn_semaphore, + ip_connections, + ) + .await; + }) + }; + + let bound_port = tokio::time::timeout(Duration::from_secs(5), port_rx) + .await + .map_err(|_| "timed out waiting for server to report its bound port")? + .map_err(|_| "server port channel closed before sending")?; + + Ok(Self { + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), bound_port), + shutdown_tx, + _root_tx: root_tx, + handle: Some(handle), + }) + } + + async fn stop(mut self) -> Result<(), Box> { + let _ = self.shutdown_tx.send(true); + if let Some(handle) = self.handle.take() { + match tokio::time::timeout(Duration::from_secs(5), handle).await { + Ok(Ok(())) => Ok(()), + Ok(Err(e)) => Err(format!("[TestServer] server task panicked: {e}").into()), + Err(_) => Err("server shutdown timed out after 5 s".into()), + } + } else { + Ok(()) + } + } +} + +impl Drop for TestServer { + fn drop(&mut self) { + let _ = self.shutdown_tx.send(true); + if let Some(handle) = self.handle.take() { + handle.abort(); + } + } +} + +async fn request_paths_over_connection( + addr: SocketAddr, + requests: &[Vec], +) -> Result<(), Box> { + let mut stream = TcpStream::connect(addr).await?; + for request in requests { + let response = if request.starts_with(b"HEAD ") { + stream.write_all(request).await?; + read_headers_only(&mut stream).await? + } else { + read_response(&mut stream, request).await? + }; + let status = status_code(&response)?; + match request.as_slice() { + req if req.starts_with(b"GET / HTTP/1.1") => { + assert_eq!(status, 200); + assert!(response_to_str(&response)?.contains("RustHost Stress Suite")); + } + req if req.starts_with(b"HEAD ") => { + assert_eq!(status, 200); + assert!(body_bytes(&response)?.is_empty()); + } + req if req.starts_with(b"GET /gallery/ ") => { + assert_eq!(status, 200); + assert!(response_to_str(&response)?.contains("thumb-a.txt")); + } + req if req.starts_with(b"GET /pages/space%20name.html ") => { + assert_eq!(status, 200); + assert!(response_to_str(&response)?.contains("Filename with a space")); + } + req if req.starts_with(b"GET /assets/logo.svg ") => { + assert_eq!(status, 200); + assert_eq!( + header_value(&response, "content-type")?.as_deref(), + Some("image/svg+xml") + ); + } + req if req.starts_with(b"GET /styles/app.css ") => { + assert_eq!(status, 200); + assert!(response_to_str(&response)?.contains("--accent")); + } + req if req.starts_with(b"GET /scripts/app.js ") => { + assert_eq!(status, 200); + assert!(response_to_str(&response)?.contains("__rusthostStressSuite")); + } + req if req.starts_with(b"GET /pages/about.html ") => { + assert_eq!(status, 200); + assert!(response_to_str(&response)?.contains("About this fixture")); + } + req if req.starts_with(b"GET /pages/nested/index.html ") => { + assert_eq!(status, 200); + assert!(response_to_str(&response)?.contains("Nested index page")); + } + _ => { + return Err(format!("unexpected request pattern: {request:?}").into()); + } + } + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn html_fixture_survives_bursty_keep_alive_load() -> Result<(), Box> { + let (tmp, site_root) = build_site_copy()?; + let server = TestServer::start(&site_root).await?; + + let mut root_stream = TcpStream::connect(server.addr).await?; + let root_response = read_response( + &mut root_stream, + b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", + ) + .await?; + assert_eq!(status_code(&root_response)?, 200); + assert!(response_to_str(&root_response)?.contains("RustHost Stress Suite")); + + let mut head_stream = TcpStream::connect(server.addr).await?; + head_stream + .write_all(b"HEAD /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await?; + let head_response = read_headers_only(&mut head_stream).await?; + assert_eq!(status_code(&head_response)?, 200); + assert!(body_bytes(&head_response)?.is_empty()); + + let mut range_stream = TcpStream::connect(server.addr).await?; + let huge_range = read_response( + &mut range_stream, + b"GET /pages/huge.html HTTP/1.1\r\nHost: localhost\r\nRange: bytes=0-255\r\n\r\n", + ) + .await?; + assert_eq!(status_code(&huge_range)?, 206); + assert!( + header_value(&huge_range, "content-range")? + .as_deref() + .is_some_and(|v| v.starts_with("bytes 0-255/")), + "expected a partial-content response with a valid Content-Range header" + ); + drop(root_stream); + drop(head_stream); + drop(range_stream); + + let burst_requests = vec![ + b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n".to_vec(), + b"GET /styles/app.css HTTP/1.1\r\nHost: localhost\r\n\r\n".to_vec(), + b"GET /scripts/app.js HTTP/1.1\r\nHost: localhost\r\n\r\n".to_vec(), + b"GET /assets/logo.svg HTTP/1.1\r\nHost: localhost\r\n\r\n".to_vec(), + b"GET /pages/about.html HTTP/1.1\r\nHost: localhost\r\n\r\n".to_vec(), + b"GET /pages/nested/index.html HTTP/1.1\r\nHost: localhost\r\n\r\n".to_vec(), + b"GET /pages/space%20name.html HTTP/1.1\r\nHost: localhost\r\n\r\n".to_vec(), + b"GET /gallery/ HTTP/1.1\r\nHost: localhost\r\n\r\n".to_vec(), + b"HEAD /pages/about.html HTTP/1.1\r\nHost: localhost\r\n\r\n".to_vec(), + ]; + + let mut tasks = Vec::new(); + for task_idx in 0..32usize { + let addr = server.addr; + let mut requests = Vec::new(); + for round in 0..16usize { + let idx = (task_idx + round) % burst_requests.len(); + requests.push(burst_requests[idx].clone()); + } + tasks.push(tokio::spawn(async move { + if let Err(err) = request_paths_over_connection(addr, &requests).await { + panic!("{err}"); + } + })); + } + + for task in tasks { + task.await?; + } + + server.stop().await?; + let _ = tmp; + Ok(()) +} diff --git a/tests/http_integration.rs b/tests/http_integration.rs index 5978624..2ea7a35 100644 --- a/tests/http_integration.rs +++ b/tests/http_integration.rs @@ -1178,8 +1178,9 @@ async fn connection_limit_rejects_second_socket() -> Result<(), Box { - let mut buf = [0u8; 16]; + let mut buf = [0u8; 256]; match tokio::time::timeout(Duration::from_secs(2), second.read(&mut buf)).await { + Ok(Ok(n)) if n > 0 => matches!(status_code(&buf[..n]), Ok(503)), Ok(Ok(0)) => true, Ok(Err(err)) if matches!( From bd4bf67e57bc114b2bf237ae60cab4ef34043d06 Mon Sep 17 00:00:00 2001 From: csd113 Date: Tue, 14 Apr 2026 15:19:01 -0700 Subject: [PATCH 3/4] readme update --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 046efff..bfda5c7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ RustHost is intentionally a **static public content server**. It does not provid - Per-IP and global connection limits - Optional HTTP to HTTPS redirect server - Custom `404` and `503` pages -- Strict Clippy and test coverage in the repo itself +- Strict Clippy and test coverage in the repo itself, including a real HTTP stress suite for mixed static assets ## Project Scope @@ -212,6 +212,11 @@ cargo clippy --all-targets --all-features -- -D warnings cargo test --all-targets ``` +The integration test suite includes `tests/html_stress.rs`, which serves the +fixture tree under `tests/fixtures/html_stress/` through the real server and +checks bursty keep-alive traffic, concurrent clients, range requests, directory +listings, percent-encoded paths, and mixed HTML/CSS/JS/SVG assets. + `unsafe` Rust is forbidden in this project. ## Documentation From 68877f6aa8b13b3b3f7d23b99401e021bd4e6e7f Mon Sep 17 00:00:00 2001 From: csd113 Date: Tue, 14 Apr 2026 16:19:52 -0700 Subject: [PATCH 4/4] Fix Clippy issues in lifecycle and html stress test --- src/runtime/lifecycle.rs | 3 +-- tests/html_stress.rs | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/runtime/lifecycle.rs b/src/runtime/lifecycle.rs index c96881f..be30540 100644 --- a/src/runtime/lifecycle.rs +++ b/src/runtime/lifecycle.rs @@ -123,8 +123,7 @@ async fn one_shot_serve(dir: PathBuf, port: u16, tor_enabled: bool, headless: bo let site_dir = canonical_dir .file_name() .and_then(|name| name.to_str()) - .map(str::to_owned) - .unwrap_or_else(|| ".".to_owned()); + .map_or_else(|| ".".to_owned(), str::to_owned); // Use the parent of `dir` as the data_dir so relative paths stay sane. let data_dir = canonical_dir diff --git a/tests/html_stress.rs b/tests/html_stress.rs index c9d7c07..9d60e69 100644 --- a/tests/html_stress.rs +++ b/tests/html_stress.rs @@ -1,4 +1,4 @@ -//! HTML stress-test suite for RustHost. +//! HTML stress-test suite for `RustHost`. //! //! This test serves the fixture tree under `tests/fixtures/html_stress` through //! the real HTTP server, then hammers it with a mixed workload: @@ -8,6 +8,7 @@ //! - a generated large HTML page with range requests use std::{ + fmt::Write as _, fs, net::{IpAddr, Ipv4Addr, SocketAddr}, path::{Path, PathBuf}, @@ -46,7 +47,7 @@ fn materialize_large_page(site_root: &Path) -> Result<(), Box\n\n\n \n Huge page\n\n\n

Huge page

\n
\n",
     );
     for i in 0..4096 {
-        body.push_str(&format!("line {:04}: RustHost static stress body\n", i));
+        let _ = writeln!(body, "line {i:04}: RustHost static stress body");
     }
     body.push_str("  
\n\n\n"); fs::write(large_path, body)?;