diff --git a/.gitignore b/.gitignore index f2aea80..01b92c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Rust build artifacts /target -/Cargo.lock # Backup files *.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..76e0be4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4208 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "argh" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "211818e820cda9ca6f167a64a5c808837366a6dfd807157c64c1304c486cd033" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c442a9d18cef5dde467405d27d461d080d68972d6d0dfd0408265b6749ec427d" +dependencies = [ + "argh_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ade012bac4db278517a0132c8c10c6427025868dca16c801087c28d5a411f1" +dependencies = [ + "serde", +] + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive 0.6.0", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bencher" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdb4953a096c551ce9ace855a604d702e6e62d77fac690575ae347571717f5" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[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.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.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 = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[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-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs 0.7.1", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "h3" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10872b55cfb02a821b69dc7cf8dc6a71d6af25eb9a79662bec4a9d016056b3be" +dependencies = [ + "bytes", + "fastrand", + "futures-util", + "http", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "h3-quinn" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2e732c8d91a74731663ac8479ab505042fbf547b9a207213ab7fbcbfc4f8b4" +dependencies = [ + "bytes", + "futures", + "h3", + "quinn", + "tokio", + "tokio-util", +] + +[[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.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.4", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.4", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "html2text" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d23156ea4dbe6b37ad48fab2da56ff27b0f6192fb5db210c44eb07bfe6e787" +dependencies = [ + "html5ever", + "tendril", + "thiserror 2.0.18", + "unicode-width", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "bytes", + "http", + "http-body", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "macro_rules_attribute" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs 0.7.1", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.6.0+3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[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 = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primp" +version = "1.2.3" +dependencies = [ + "criterion", + "http", + "primp-h2", + "primp-reqwest", + "primp-rustls", + "rand 0.10.1", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "primp-h2" +version = "0.4.13" +dependencies = [ + "atomic-waker", + "bytes", + "env_logger", + "fnv", + "futures-core", + "futures-sink", + "hex", + "http", + "indexmap", + "quickcheck", + "rand 0.8.6", + "serde", + "serde_json", + "slab", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "walkdir", +] + +[[package]] +name = "primp-hyper" +version = "1.9.0" +dependencies = [ + "atomic-waker", + "bytes", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pretty_env_logger", + "primp-h2", + "serde", + "serde_json", + "smallvec", + "spmc", + "tokio", + "tokio-test", + "tokio-util", + "tracing", + "want", +] + +[[package]] +name = "primp-hyper-rustls" +version = "0.27.9" +dependencies = [ + "cfg-if", + "http", + "http-body-util", + "log", + "primp-hyper", + "primp-hyper-util", + "primp-rustls", + "primp-tokio-rustls", + "rustls-native-certs", + "rustls-platform-verifier", + "tokio", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "primp-hyper-util" +version = "0.1.20" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "pnet_datalink", + "pretty_env_logger", + "primp-hyper", + "socket2", + "system-configuration", + "tokio", + "tokio-test", + "tower-layer", + "tower-service", + "tower-test", + "tracing", + "windows-registry", +] + +[[package]] +name = "primp-python" +version = "1.2.3" +dependencies = [ + "anyhow", + "bytes", + "encoding_rs", + "foldhash 0.2.0", + "html2text", + "http", + "indexmap", + "mime", + "primp", + "pyo3", + "pyo3-async-runtimes", + "pyo3-log", + "pythonize", + "rand 0.10.1", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "primp-reqwest" +version = "0.13.3" +dependencies = [ + "base64", + "brotli", + "bytes", + "cookie", + "cookie_store", + "doc-comment", + "encoding_rs", + "env_logger", + "flate2", + "futures-channel", + "futures-core", + "futures-util", + "h3", + "h3-quinn", + "hickory-resolver", + "http", + "http-body", + "http-body-util", + "hyper-tls", + "js-sys", + "libc", + "log", + "mime", + "mime_guess", + "native-tls", + "num_cpus", + "once_cell", + "percent-encoding", + "pin-project-lite", + "primp-h2", + "primp-hyper", + "primp-hyper-rustls", + "primp-hyper-util", + "primp-rustls", + "primp-tokio-rustls", + "quinn", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "wasm-streams", + "web-sys", + "webpki-roots", + "zstd", +] + +[[package]] +name = "primp-rustls" +version = "0.23.40" +dependencies = [ + "aws-lc-rs", + "base64", + "bencher", + "brotli", + "brotli-decompressor", + "env_logger", + "hashbrown 0.16.1", + "hex", + "log", + "macro_rules_attribute", + "num-bigint", + "once_cell", + "rcgen 0.13.2", + "ring", + "rustls-pki-types", + "rustls-test", + "rustls-webpki", + "rustversion", + "serde", + "serde_json", + "subtle", + "time", + "webpki-roots", + "x509-parser 0.16.0", + "zeroize", + "zlib-rs", + "zstd", +] + +[[package]] +name = "primp-tokio-rustls" +version = "0.26.4" +dependencies = [ + "argh", + "futures-util", + "lazy_static", + "primp-rustls", + "rcgen 0.14.8", + "tokio", + "webpki-roots", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "pyo3" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" +dependencies = [ + "anyhow", + "indexmap", + "inventory", + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-async-runtimes" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7364a95bf00e8377bbf9b0f09d7ff9715a29d8fcf93b47d1a967363b973178" +dependencies = [ + "futures-channel", + "futures-util", + "once_cell", + "pin-project-lite", + "pyo3", + "tokio", +] + +[[package]] +name = "pyo3-build-config" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-log" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c2ec80932c5c3b2d4fbc578c9b56b2d4502098587edb8bef5b6bfcad43682e" +dependencies = [ + "arc-swap", + "log", + "pyo3", +] + +[[package]] +name = "pyo3-macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "pythonize" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b79f670c9626c8b651c0581011b57b6ba6970bb69faf01a7c4c0cfc81c43f95" +dependencies = [ + "pyo3", + "serde", +] + +[[package]] +name = "quickcheck" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c589f335db0f6aaa168a7cd27b1fc6920f5e1470c804f814d9cd6e62a0f70b" +dependencies = [ + "rand 0.10.1", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna 0.5.2", +] + +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser 0.18.1", + "yasna 0.6.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-test" +version = "0.1.0" +dependencies = [ + "primp-rustls", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spmc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a8428da277a8e3a15271d79943e80ccc2ef254e78813a166a08d65e4c3ece5" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "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 = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "async-compression", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tower-test" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4546773ffeab9e4ea02b8872faa49bb616a80a7da66afc2f32688943f97efa7" +dependencies = [ + "futures-util", + "pin-project", + "tokio", + "tokio-test", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "serde", + "serde_json", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af5ec93229ad9ccd0a545a516dec76dc276613f278f6a91aa6b463d5b33d42d0" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom", + "oid-registry 0.7.1", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs 0.7.1", + "data-encoding", + "der-parser 10.0.0", + "lazy_static", + "nom", + "oid-registry 0.8.1", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/README.md b/README.md index 2321d77..32026be 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,11 @@ print(resp.text) | Browser | Profiles | |:--------|:---------| -| Chrome | `chrome_144`, `chrome_145`, `chrome_146`, `chrome` | +| Chrome | `chrome_144`, `chrome_145`, `chrome_146`, `chrome_147`, `chrome_148`, `chrome` | | Safari | `safari_18.5`, `safari_26`, `safari_26.3`, `safari` | -| Edge | `edge_144`, `edge_145`, `edge_146`, `edge` | +| Edge | `edge_144`, `edge_145`, `edge_146`, `edge_147`, `edge_148`, `edge` | | Firefox | `firefox_140`, `firefox_146`, `firefox_147`, `firefox_148`, `firefox` | -| Opera | `opera_126`, `opera_127`, `opera_128`, `opera_129`, `opera` | +| Opera | `opera_126`, `opera_127`, `opera_128`, `opera_129`, `opera_130`, `opera_131`, `opera` | | Random | `random` | **OS:** `android`, `ios`, `linux`, `macos`, `windows`, `random` diff --git a/crates/primp-h2/src/client.rs b/crates/primp-h2/src/client.rs index dbfa3fe..3830ceb 100644 --- a/crates/primp-h2/src/client.rs +++ b/crates/primp-h2/src/client.rs @@ -137,7 +137,7 @@ use crate::codec::{Codec, SendError, UserError}; use crate::ext::Protocol; -use crate::frame::{Headers, Pseudo, Reason, Settings, StreamId}; +use crate::frame::{Headers, Pseudo, Reason, Settings, StreamId, WindowUpdate}; use crate::proto::{self, Error}; use crate::{FlowControl, PingPong, RecvStream, SendStream}; @@ -321,6 +321,12 @@ pub struct Builder { /// Initial target window size for new connections. initial_target_connection_window_size: Option, + /// Extra receive window capacity to add to new streams. + /// + /// When set, after creating a new locally-initiated stream, a WINDOW_UPDATE + /// frame will be sent to increase the stream's receive window by this amount. + initial_stream_window_increment: Option, + /// Maximum amount of bytes to "buffer" for writing per stream. max_send_buffer_size: usize, @@ -670,6 +676,7 @@ impl Builder { reset_stream_max: proto::DEFAULT_RESET_STREAM_MAX, pending_accept_reset_stream_max: proto::DEFAULT_REMOTE_RESET_STREAM_MAX, initial_target_connection_window_size: None, + initial_stream_window_increment: None, initial_max_send_streams: usize::MAX, settings: Default::default(), stream_id: 1.into(), @@ -750,6 +757,22 @@ impl Builder { self } + /// Sets extra receive window capacity to add to new locally-initiated streams. + /// + /// When set, after creating a new stream, a `WINDOW_UPDATE` frame will be + /// sent to increase the stream's receive window by this amount. + /// + /// This is used for browser fingerprinting (e.g. Firefox sends a stream-level + /// WINDOW_UPDATE of 12451840 on the first stream). + /// + /// # Default + /// + /// Default is `None`. + pub fn initial_stream_window_size_increment(&mut self, size: u32) -> &mut Self { + self.initial_stream_window_increment = Some(size); + self + } + /// Indicates the size (in octets) of the largest HTTP/2 frame payload that the /// configured client is able to accept. /// @@ -1418,6 +1441,7 @@ where headers_pseudo_order: builder.headers_pseudo_order.clone(), headers_priority: builder.headers_priority, headers_order: builder.headers_order.clone(), + initial_stream_window_increment: builder.initial_stream_window_increment, }, ); let send_request = SendRequest { @@ -1427,7 +1451,17 @@ where let mut connection = Connection { inner }; if let Some(sz) = builder.initial_target_connection_window_size { - connection.set_target_window_size(sz); + connection.inner.set_target_window_size(sz); + // Buffer connection WU immediately so it's sent before any HEADERS frames + if let Some(incr) = connection.inner.streams().unclaimed_connection_window() { + let frame = WindowUpdate::new(StreamId::zero(), incr); + connection + .inner + .codec + .buffer(frame.into()) + .expect("invalid WINDOW_UPDATE frame"); + connection.inner.streams().inc_connection_window(incr); + } } Ok((send_request, connection)) diff --git a/crates/primp-h2/src/proto/connection.rs b/crates/primp-h2/src/proto/connection.rs index 353f82d..fc2652a 100644 --- a/crates/primp-h2/src/proto/connection.rs +++ b/crates/primp-h2/src/proto/connection.rs @@ -21,7 +21,7 @@ where P: Peer, { /// Read / write frame values - codec: Codec>, + pub(crate) codec: Codec>, inner: ConnectionInner, } @@ -86,6 +86,7 @@ pub(crate) struct Config { pub headers_pseudo_order: Option, pub headers_priority: Option<(u8, u32, bool)>, pub headers_order: Option>, + pub initial_stream_window_increment: Option, } #[derive(Debug)] @@ -129,6 +130,7 @@ where headers_pseudo_order: config.headers_pseudo_order.clone(), headers_priority: config.headers_priority, headers_order: config.headers_order.clone(), + initial_stream_window_increment: config.initial_stream_window_increment, } } let streams = Streams::new(streams_config(&config)); diff --git a/crates/primp-h2/src/proto/streams/mod.rs b/crates/primp-h2/src/proto/streams/mod.rs index b43809b..c9397ff 100644 --- a/crates/primp-h2/src/proto/streams/mod.rs +++ b/crates/primp-h2/src/proto/streams/mod.rs @@ -79,6 +79,12 @@ pub struct Config { /// Whether to include PRIORITY flag in HEADERS frames pub headers_priority: Option<(u8, u32, bool)>, pub headers_order: Option>, + + /// Extra receive window capacity to add to new locally-initiated streams. + /// + /// When set, after creating a new stream, a WINDOW_UPDATE frame will be + /// buffered to increase the stream's receive window by this amount. + pub initial_stream_window_increment: Option, } trait DebugStructExt<'a, 'b> { diff --git a/crates/primp-h2/src/proto/streams/recv.rs b/crates/primp-h2/src/proto/streams/recv.rs index 9a10ca3..1d98caa 100644 --- a/crates/primp-h2/src/proto/streams/recv.rs +++ b/crates/primp-h2/src/proto/streams/recv.rs @@ -30,15 +30,17 @@ pub(super) struct Recv { /// Any streams with a higher ID are ignored. /// /// This starts as MAX, but is lowered when a GOAWAY is received. - /// - /// > After sending a GOAWAY frame, the sender can discard frames for - /// > streams initiated by the receiver with identifiers higher than - /// > the identified last stream. max_stream_id: StreamId, /// Streams that have pending window updates pending_window_updates: store::Queue, + /// A pending initial WINDOW_UPDATE for a newly created stream. + /// Set by add_stream_window, consumed in send_initial_window_update. + /// Uses a separate path from pending_window_updates to avoid + /// setting is_pending_window_update (which would prevent stream release). + initial_window_update: Option<(store::Key, WindowSize)>, + /// New streams to be accepted pending_accept: store::Queue, @@ -99,6 +101,7 @@ impl Recv { in_flight_data: 0 as WindowSize, next_stream_id: Ok(next_stream_id.into()), pending_window_updates: store::Queue::new(), + initial_window_update: None, last_processed_id: StreamId::ZERO, max_stream_id: StreamId::MAX, pending_accept: store::Queue::new(), @@ -502,6 +505,41 @@ impl Recv { self.clear_recv_buffer(stream); } + /// Assign extra receive window capacity to a stream. + /// + /// This queues a WINDOW_UPDATE frame to increase the stream's receive + /// window by `incr`. Used for browser fingerprinting (e.g. Firefox adds + /// 12451840 to the first stream's receive window). + /// + /// Unlike `release_capacity`, this does not use the `pending_window_updates` + /// queue (which sets `is_pending_window_update` and prevents stream release). + /// Instead, it stores a key that will be checked in `send_stream_window_updates`. + pub fn add_stream_window( + &mut self, + incr: WindowSize, + stream: &mut store::Ptr, + _task: &mut Option, + ) { + tracing::trace!( + "add_stream_window; stream={:?}; incr={}; available={}, in_flight={}", + stream.id, + incr, + stream.recv_flow.available(), + stream.in_flight_recv_data, + ); + + // Assign capacity and immediately inc_window to keep the flow control + // state balanced (unclaimed stays at 0 so release_capacity doesn't + // try to queue a WU for this). The WU frame will be sent later via + // initial_window_update in send_stream_window_updates. + let _res = stream.recv_flow.assign_capacity(incr); + debug_assert!(_res.is_ok()); + let _res = stream.recv_flow.inc_window(incr); + debug_assert!(_res.is_ok()); + + self.initial_window_update = Some((stream.key(), incr)); + } + /// Set the "target" connection window size. /// /// By default, all new connections start with 64kb of window size. As @@ -1048,7 +1086,11 @@ impl Recv { } } - fn clear_stream_window_update_queue(&mut self, store: &mut Store, counts: &mut Counts) { + pub(super) fn clear_stream_window_update_queue( + &mut self, + store: &mut Store, + counts: &mut Counts, + ) { while let Some(stream) = self.pending_window_updates.pop(store) { counts.transition(stream, |_, stream| { tracing::trace!("clear_stream_window_update_queue; stream={:?}", stream.id); @@ -1069,6 +1111,16 @@ impl Recv { } } + pub fn unclaimed_connection_window(&self) -> Option { + self.flow.unclaimed_capacity() + } + + pub fn inc_connection_window(&mut self, incr: WindowSize) { + self.flow + .inc_window(incr) + .expect("connection window overflow"); + } + pub fn poll_complete( &mut self, cx: &mut Context, @@ -1173,6 +1225,28 @@ impl Recv { } } + /// Send any buffered initial stream WINDOW_UPDATE for a newly created + /// stream (from `initial_stream_window_increment`). + pub fn send_initial_window_update( + &mut self, + cx: &mut Context, + dst: &mut Codec>, + store: &mut Store, + ) -> Poll> + where + T: AsyncWrite + Unpin, + B: Buf, + { + if let Some((key, incr)) = self.initial_window_update.take() { + ready!(dst.poll_ready(cx))?; + let stream = &mut store[key]; + let frame = frame::WindowUpdate::new(stream.id, incr); + dst.buffer(frame.into()) + .expect("invalid WINDOW_UPDATE frame"); + } + Poll::Ready(Ok(())) + } + pub fn next_incoming(&mut self, store: &mut Store) -> Option { self.pending_accept.pop(store).map(|ptr| ptr.key()) } diff --git a/crates/primp-h2/src/proto/streams/streams.rs b/crates/primp-h2/src/proto/streams/streams.rs index 4957617..9074f91 100644 --- a/crates/primp-h2/src/proto/streams/streams.rs +++ b/crates/primp-h2/src/proto/streams/streams.rs @@ -85,6 +85,9 @@ struct Inner { /// Whether to include PRIORITY flag in HEADERS frames headers_priority: Option<(u8, u32, bool)>, headers_order: Option>, + + /// Extra receive window capacity to add to new locally-initiated streams. + initial_stream_window_increment: Option, } #[derive(Debug)] @@ -181,6 +184,19 @@ where .clear_expired_reset_streams(&mut me.store, &mut me.counts); } + /// Returns the unclaimed connection window capacity, if any. + pub fn unclaimed_connection_window(&self) -> Option { + let me = self.inner.lock().unwrap(); + me.actions.recv.unclaimed_connection_window() + } + + /// Increments the connection window by the given amount (matching a + /// WINDOW_UPDATE frame that was already buffered). + pub fn inc_connection_window(&self, incr: WindowSize) { + let mut me = self.inner.lock().unwrap(); + me.actions.recv.inc_connection_window(incr); + } + pub fn poll_complete( &mut self, cx: &mut Context, @@ -321,6 +337,15 @@ where // closed state. debug_assert!(!stream.state.is_closed()); + // Apply initial stream window increment, if configured. + // This sends a WINDOW_UPDATE frame to bump the stream's receive window + // (e.g. Firefox adds 12451840 to the first stream's receive window). + if let Some(incr) = me.initial_stream_window_increment { + me.actions + .recv + .add_stream_window(incr, &mut stream, &mut me.actions.task); + } + // TODO: ideally, OpaqueStreamRefs::new would do this, but we're holding // the lock, so it can't. me.refs += 1; @@ -441,6 +466,7 @@ impl Inner { headers_pseudo_order: config.headers_pseudo_order, headers_priority: config.headers_priority, headers_order: config.headers_order, + initial_stream_window_increment: config.initial_stream_window_increment, })) } @@ -920,10 +946,7 @@ impl Inner { T: AsyncWrite + Unpin, B: Buf, { - let mut send_buffer = send_buffer.inner.lock().unwrap(); - let send_buffer = &mut *send_buffer; - - // Send WINDOW_UPDATE frames first + // Send WINDOW_UPDATE frames first for normal flow control updates // // TODO: It would probably be better to interleave updates w/ data // frames. @@ -932,14 +955,31 @@ impl Inner { .recv .poll_complete(cx, &mut self.store, &mut self.counts, dst))?; - // Send any other pending frames - ready!(self.actions.send.poll_complete( - cx, - send_buffer, - &mut self.store, - &mut self.counts, - dst - ))?; + // Send any other pending frames (HEADERS, DATA, etc.) + { + let mut send_buffer = send_buffer.inner.lock().unwrap(); + let send_buffer = &mut *send_buffer; + ready!(self.actions.send.poll_complete( + cx, + send_buffer, + &mut self.store, + &mut self.counts, + dst + ))?; + } + + // Send initial stream window updates AFTER HEADERS to ensure ordering: + // SETTINGS, WU(conn), HEADERS, WU(stream) — matching real browsers + ready!(self + .actions + .recv + .send_initial_window_update(cx, dst, &mut self.store))?; + + // Flush the initial stream WU to the wire immediately. + // Without this, the WU sits in the codec buffer and the next poll + // may process the server's response (and close the stream) before + // the WU is ever flushed. + ready!(dst.flush(cx))?; // Nothing else to do, track the task self.actions.task = Some(cx.waker().clone()); diff --git a/crates/primp-h2/src/server.rs b/crates/primp-h2/src/server.rs index 61a5181..17d9ae1 100644 --- a/crates/primp-h2/src/server.rs +++ b/crates/primp-h2/src/server.rs @@ -1496,6 +1496,7 @@ where headers_pseudo_order: None, headers_priority: None, headers_order: None, + initial_stream_window_increment: None, }, ); diff --git a/crates/primp-hyper-util/src/client/legacy/client.rs b/crates/primp-hyper-util/src/client/legacy/client.rs index 74943de..e68edce 100644 --- a/crates/primp-hyper-util/src/client/legacy/client.rs +++ b/crates/primp-hyper-util/src/client/legacy/client.rs @@ -1375,6 +1375,24 @@ impl Builder { self } + /// Sets extra receive window capacity to add to new locally-initiated streams. + /// + /// When set, after creating a new stream, a WINDOW_UPDATE frame will be + /// sent to increase the stream's receive window by this amount. + /// This is used for browser fingerprinting (e.g. Firefox adds 12451840 + /// to the first stream's receive window). + #[cfg(feature = "http2")] + #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] + pub fn http2_initial_stream_window_size_increment( + &mut self, + sz: impl Into>, + ) -> &mut Self { + if let Some(sz) = sz.into() { + self.h2_builder.initial_stream_window_size_increment(sz); + } + self + } + /// Sets the initial maximum of locally initiated (send) streams. /// /// This value will be overwritten by the value included in the initial diff --git a/crates/primp-hyper/Cargo.toml b/crates/primp-hyper/Cargo.toml index 9e5c6e4..acbe779 100644 --- a/crates/primp-hyper/Cargo.toml +++ b/crates/primp-hyper/Cargo.toml @@ -120,6 +120,7 @@ version = "0.4" path = "../primp-h2" package = "primp-h2" default-features = false +features = ["unstable"] optional = true [dependencies.http] diff --git a/crates/primp-hyper/src/client/conn/http2.rs b/crates/primp-hyper/src/client/conn/http2.rs index 9cc468b..cda43c4 100644 --- a/crates/primp-hyper/src/client/conn/http2.rs +++ b/crates/primp-hyper/src/client/conn/http2.rs @@ -564,6 +564,17 @@ where self } + /// Sets extra receive window capacity to add to new locally-initiated streams. + /// + /// When set, after creating a new stream, a WINDOW_UPDATE frame will be + /// sent to increase the stream's receive window by this amount. + /// This is used for browser fingerprinting (e.g. Firefox adds 12451840 + /// to the first stream's receive window). + pub fn initial_stream_window_size_increment(&mut self, size: u32) -> &mut Self { + self.h2_builder.initial_stream_window_increment = Some(size); + self + } + /// Configures the maximum number of local resets due to protocol errors made by the remote end. /// /// See the documentation of [`h2::client::Builder::max_local_error_reset_streams`] for more diff --git a/crates/primp-hyper/src/proto/h2/client.rs b/crates/primp-hyper/src/proto/h2/client.rs index bfb185f..b88542a 100644 --- a/crates/primp-hyper/src/proto/h2/client.rs +++ b/crates/primp-hyper/src/proto/h2/client.rs @@ -84,6 +84,7 @@ pub(crate) struct Config { pub(crate) headers_priority: Option<(u8, u32, bool)>, pub(crate) headers_order: Option>, pub(crate) initial_stream_id: Option, + pub(crate) initial_stream_window_increment: Option, } impl Default for Config { @@ -112,6 +113,7 @@ impl Default for Config { headers_priority: None, headers_order: None, initial_stream_id: None, + initial_stream_window_increment: None, } } } @@ -166,6 +168,9 @@ fn new_builder(config: &Config) -> Builder { if let Some(stream_id) = config.initial_stream_id { builder.initial_stream_id(stream_id); } + if let Some(incr) = config.initial_stream_window_increment { + builder.initial_stream_window_size_increment(incr); + } builder } diff --git a/crates/primp-hyper/tests/client.rs b/crates/primp-hyper/tests/client.rs index 14ffbb5..167871b 100644 --- a/crates/primp-hyper/tests/client.rs +++ b/crates/primp-hyper/tests/client.rs @@ -1798,10 +1798,7 @@ mod conn { support::runtime().block_on(rx).unwrap(); // Aborts the body in an abnormal fashion. - let _ = sender.try_send(Err(Box::new(std::io::Error::new( - io::ErrorKind::Other, - "body write aborted", - )))); + let _ = sender.try_send(Err(Box::new(std::io::Error::other("body write aborted")))); }); let req = Request::builder() diff --git a/crates/primp-hyper/tests/server.rs b/crates/primp-hyper/tests/server.rs index 5b947e7..ba43823 100644 --- a/crates/primp-hyper/tests/server.rs +++ b/crates/primp-hyper/tests/server.rs @@ -3404,10 +3404,7 @@ where } if pos == buf.len() { - return Err(io::Error::new( - io::ErrorKind::Other, - "read_until buffer filled", - )); + return Err(io::Error::other("read_until buffer filled")); } } buf.truncate(pos); diff --git a/crates/primp-python/README.md b/crates/primp-python/README.md index 676c2e6..84c346d 100644 --- a/crates/primp-python/README.md +++ b/crates/primp-python/README.md @@ -55,11 +55,11 @@ asyncio.run(main()) | Browser | Profiles | |:--------|:---------| -| Chrome | `chrome_144`, `chrome_145`, `chrome_146`, `chrome` | +| Chrome | `chrome_144`, `chrome_145`, `chrome_146`, `chrome_147`, `chrome_148`, `chrome` | | Safari | `safari_18.5`, `safari_26`, `safari_26.3`, `safari` | -| Edge | `edge_144`, `edge_145`, `edge_146`, `edge` | +| Edge | `edge_144`, `edge_145`, `edge_146`, `edge_147`, `edge_148`, `edge` | | Firefox | `firefox_140`, `firefox_146`, `firefox_147`, `firefox_148`, `firefox` | -| Opera | `opera_126`, `opera_127`, `opera_128`, `opera_129`, `opera` | +| Opera | `opera_126`, `opera_127`, `opera_128`, `opera_129`, `opera_130`, `opera_131`, `opera` | | Random | `random` | **OS:** `android`, `ios`, `linux`, `macos`, `windows`, `random` diff --git a/crates/primp-python/docs/impersonate.md b/crates/primp-python/docs/impersonate.md index 8a08322..98b7c40 100644 --- a/crates/primp-python/docs/impersonate.md +++ b/crates/primp-python/docs/impersonate.md @@ -24,11 +24,11 @@ client = primp.Client(impersonate="random") | Browser | Profiles | |:--------|:---------| -| Chrome | `chrome_144`, `chrome_145`, `chrome_146`, `chrome` | +| Chrome | `chrome_144`, `chrome_145`, `chrome_146`, `chrome_147`, `chrome_148`, `chrome` | | Safari | `safari_18.5`, `safari_26`, `safari_26.3`, `safari` | -| Edge | `edge_144`, `edge_145`, `edge_146`, `edge` | +| Edge | `edge_144`, `edge_145`, `edge_146`, `edge_147`, `edge_148`, `edge` | | Firefox | `firefox_140`, `firefox_146`, `firefox_147`, `firefox_148`, `firefox` | -| Opera | `opera_126`, `opera_127`, `opera_128`, `opera_129`, `opera` | +| Opera | `opera_126`, `opera_127`, `opera_128`, `opera_129`, `opera_130`, `opera_131`, `opera` | | Random | `random` | Specific versions (e.g., `chrome_146`) pin a single browser version. Family selectors (e.g., `chrome`) pick a random version from that browser family. `random` picks any browser randomly. diff --git a/crates/primp-python/src/async/response.rs b/crates/primp-python/src/async/response.rs index 2fadd8d..203759a 100644 --- a/crates/primp-python/src/async/response.rs +++ b/crates/primp-python/src/async/response.rs @@ -1,38 +1,18 @@ use std::sync::Arc; -use anyhow::Result; use bytes::Bytes; use encoding_rs::{Encoding, UTF_8}; -use html2text::{ - from_read, from_read_with_decorator, - render::{RichDecorator, TrivialDecorator}, -}; use pyo3::{ prelude::*, types::{PyBytes, PyDict, PyString}, - IntoPyObjectExt, PyErr, + PyErr, }; use pythonize::pythonize; -use serde_json::from_slice; use tokio::sync::Mutex as TMutex; use crate::client_builder::IndexMapSSR; -use crate::error::{body_collection_error, convert_reqwest_error, BodyError, PrimpErrorEnum}; -use crate::traits::HeadersTraits; -use crate::utils::extract_encoding; - -/// Collect body bytes from a response using a pre-allocated buffer. -/// Uses `Vec::with_capacity(8 * 1024)` for efficient buffer allocation. -async fn collect_body_bytes(resp: &mut ::primp::Response) -> Result { - let mut buf = Vec::with_capacity(8 * 1024); - loop { - match resp.chunk().await { - Ok(Some(chunk)) => buf.extend_from_slice(&chunk), - Ok(None) => break Ok(Bytes::from(buf)), - Err(e) => return Err(body_collection_error(&e.to_string())), - } - } -} +use crate::error::convert_reqwest_error; +use crate::response_shared; /// A struct representing an async HTTP response. /// @@ -93,60 +73,13 @@ impl AsyncResponse { /// Get response content as bytes (sync - blocks until content is read) #[getter] fn get_content<'rs>(&mut self, py: Python<'rs>) -> PyResult> { - if !self.streaming { - if let Some(content) = &self._content { - let cloned = content.clone_ref(py); - return Ok(cloned.into_bound(py)); - } - } - - let resp = Arc::clone(&self.resp); - let runtime = crate::get_runtime(py); - let bytes: Bytes = py.detach(|| { - runtime.block_on(async { - let mut resp_guard = resp.lock().await; - match resp_guard.as_mut() { - Some(r) => collect_body_bytes(r).await, - None => Err(BodyError::new_err( - "Response body already consumed or moved", - )), - } - }) - })?; - - let content = PyBytes::new(py, &bytes); - - if !self.streaming { - self._content = Some(content.clone().unbind()); - } - Ok(content) + response_shared::get_content(&self.resp, &mut self._content, self.streaming, py) } /// Get character encoding (sync) #[getter] fn get_encoding(&mut self, py: Python<'_>) -> PyResult { - if let Some(encoding) = self._encoding.as_ref() { - return Ok(encoding.clone()); - } - - let resp = Arc::clone(&self.resp); - let runtime = crate::get_runtime(py); - let encoding = py.detach(|| { - runtime.block_on(async { - let resp_guard = resp.lock().await; - match resp_guard.as_ref() { - Some(r) => { - Ok::(extract_encoding(r.headers()).name().to_string()) - } - None => Err(BodyError::new_err( - "Response body already consumed or moved", - )), - } - }) - })?; - - self._encoding = Some(encoding.clone()); - Ok(encoding) + response_shared::get_encoding(&self.resp, &mut self._encoding, py) } /// Set character encoding @@ -161,136 +94,53 @@ impl AsyncResponse { /// Get response text (sync - blocks until content is read) #[getter] fn text<'rs>(&mut self, py: Python<'rs>) -> PyResult> { - let content = self.get_content(py)?.unbind(); - let encoding = self.get_encoding(py)?; - let raw_bytes = content.as_bytes(py); - let encoding = Encoding::for_label(encoding.as_bytes()).unwrap_or(UTF_8); - let (text, _, _) = encoding.decode(raw_bytes); - text.into_pyobject_or_pyerr(py) + response_shared::text( + &self.resp, + &mut self._content, + &mut self._encoding, + self.streaming, + py, + ) } /// Get response headers (sync) #[getter] fn get_headers<'rs>(&mut self, py: Python<'rs>) -> PyResult> { - if let Some(headers) = &self._headers { - return headers.clone().into_pyobject(py); - } - - let resp = Arc::clone(&self.resp); - let runtime = crate::get_runtime(py); - let headers: IndexMapSSR = runtime.block_on(async { - let resp_guard = resp.lock().await; - match resp_guard.as_ref() { - Some(r) => Ok(r.headers().to_indexmap()), - None => Err(BodyError::new_err( - "Response body already consumed or moved", - )), - } - })?; - - let py_dict = headers.clone().into_pyobject(py)?; - self._headers = Some(headers); - Ok(py_dict) + response_shared::get_headers(&self.resp, &mut self._headers, py) } /// Get response cookies (sync) #[getter] fn get_cookies<'rs>(&mut self, py: Python<'rs>) -> PyResult> { - if let Some(cookies) = &self._cookies { - return cookies.clone().into_pyobject(py); - } - - let resp = Arc::clone(&self.resp); - let runtime = crate::get_runtime(py); - let cookies: IndexMapSSR = runtime.block_on(async { - let resp_guard = resp.lock().await; - match resp_guard.as_ref() { - Some(r) => Ok(crate::extract_cookies_to_indexmap(r.headers())), - None => Err(BodyError::new_err( - "Response body already consumed or moved", - )), - } - })?; - - let py_dict = cookies.clone().into_pyobject(py)?; - self._cookies = Some(cookies); - Ok(py_dict) + response_shared::get_cookies(&self.resp, &mut self._cookies, py) } /// Get HTML converted to Markdown (sync) #[getter] - fn text_markdown(&mut self, py: Python<'_>) -> Result { - let content = self.get_content(py)?.unbind(); - let raw_bytes = content.as_bytes(py); - let text = py.detach(|| from_read(raw_bytes, 100))?; - Ok(text) + fn text_markdown(&mut self, py: Python<'_>) -> PyResult { + response_shared::text_markdown(&self.resp, &mut self._content, self.streaming, py) } /// Get HTML converted to plain text (sync) #[getter] - fn text_plain(&mut self, py: Python<'_>) -> Result { - let content = self.get_content(py)?.unbind(); - let raw_bytes = content.as_bytes(py); - let text = - py.detach(|| from_read_with_decorator(raw_bytes, 100, TrivialDecorator::new()))?; - Ok(text) + fn text_plain(&mut self, py: Python<'_>) -> PyResult { + response_shared::text_plain(&self.resp, &mut self._content, self.streaming, py) } /// Get HTML converted to rich text (sync) #[getter] - fn text_rich(&mut self, py: Python<'_>) -> Result { - let content = self.get_content(py)?.unbind(); - let raw_bytes = content.as_bytes(py); - let text = py.detach(|| from_read_with_decorator(raw_bytes, 100, RichDecorator::new()))?; - Ok(text) + fn text_rich(&mut self, py: Python<'_>) -> PyResult { + response_shared::text_rich(&self.resp, &mut self._content, self.streaming, py) } /// Parse response body as JSON (sync) fn json<'rs>(&mut self, py: Python<'rs>) -> PyResult> { - let content = self.get_content(py)?.unbind(); - let raw_bytes = content.as_bytes(py); - let json_value: serde_json::Value = match from_slice(raw_bytes) { - Ok(v) => v, - Err(e) => { - let json_module = py.import("json")?; - let error_type = json_module.getattr("JSONDecodeError")?; - let doc = String::from_utf8_lossy(raw_bytes).to_string(); - let msg = e.to_string(); - let pos = e.line().saturating_sub(1); - return Err(PyErr::from_value(error_type.call1((&msg, &doc, pos))?)); - } - }; - let result = pythonize(py, &json_value)?; - Ok(result) + response_shared::json(&self.resp, &mut self._content, self.streaming, py) } /// Raise HTTPError for 4xx/5xx status codes (sync) fn raise_for_status(&self) -> PyResult<()> { - if self.status_code >= 400 { - let reason = if self.status_code < 600 { - match self.status_code { - 400 => "Bad Request", - 401 => "Unauthorized", - 403 => "Forbidden", - 404 => "Not Found", - 405 => "Method Not Allowed", - 409 => "Conflict", - 500 => "Internal Server Error", - 502 => "Bad Gateway", - 503 => "Service Unavailable", - _ => "Error", - } - } else { - "Unknown Error" - }; - - return Err(PyErr::from(PrimpErrorEnum::HttpStatus( - self.status_code, - reason.to_string(), - self.url.clone(), - ))); - } - Ok(()) + response_shared::raise_for_status(self.status_code, &self.url) } /// Read remaining content into memory (async) @@ -300,7 +150,7 @@ impl AsyncResponse { let future = async move { let mut resp_guard = resp.lock().await; let bytes = match resp_guard.as_mut() { - Some(r) => match collect_body_bytes(r).await { + Some(r) => match response_shared::collect_body_bytes(r).await { Ok(buf) => buf, Err(e) => return Err(e), }, @@ -330,7 +180,7 @@ impl AsyncResponse { let future = async move { let mut resp_guard = resp.lock().await; let bytes = match resp_guard.as_mut() { - Some(r) => match collect_body_bytes(r).await { + Some(r) => match response_shared::collect_body_bytes(r).await { Ok(buf) => buf, Err(e) => return Err(e), }, @@ -346,30 +196,42 @@ impl AsyncResponse { /// Parse response content as JSON (async - reads all remaining content) fn json_async<'py>(&mut self, py: Python<'py>) -> PyResult> { + use pyo3::exceptions::PyValueError; use pyo3_async_runtimes::tokio::future_into_py; let resp = Arc::clone(&self.resp); - let bytes_future = future_into_py(py, async move { + let future = future_into_py(py, async move { let mut resp_guard = resp.lock().await; let bytes = match resp_guard.as_mut() { - Some(r) => match collect_body_bytes(r).await { + Some(r) => match response_shared::collect_body_bytes(r).await { Ok(buf) => buf, Err(e) => return Err(e), }, None => Bytes::new(), }; - Ok::, PyErr>(bytes.to_vec()) + let json_value: serde_json::Value = match serde_json::from_slice(&bytes) { + Ok(v) => v, + Err(e) => { + let doc = String::from_utf8_lossy(&bytes).to_string(); + let msg = e.to_string(); + let pos = e.line().saturating_sub(1); + return Err(Python::attach(|py| { + let json_module = py.import("json")?; + let error_type = json_module.getattr("JSONDecodeError")?; + let py_err = error_type.call1((&msg, &doc, pos))?; + Ok::<_, PyErr>(PyErr::from_value(py_err)) + })?); + } + }; + let py_obj: Py = Python::attach(|py| { + let bound = pythonize(py, &json_value) + .map_err(|e| PyErr::new::(e.to_string()))?; + Ok::<_, PyErr>(bound.unbind()) + })?; + Ok(py_obj) })?; - let json_code = c" -import json as _json -async def _parse_json(coro): - data = await coro - return _json.loads(bytes(data)) -_parse_json -"; - let parse_fn = py.eval(json_code, None, None)?.call0()?; - parse_fn.call1((bytes_future,)) + Ok(future) } #[pyo3(signature = (chunk_size=None))] diff --git a/crates/primp-python/src/impersonate.rs b/crates/primp-python/src/impersonate.rs index e331196..a96e104 100644 --- a/crates/primp-python/src/impersonate.rs +++ b/crates/primp-python/src/impersonate.rs @@ -29,17 +29,23 @@ pub fn parse_impersonate(s: &str) -> Result { "chrome_144" => Ok(Impersonate::ChromeV144), "chrome_145" => Ok(Impersonate::ChromeV145), "chrome_146" => Ok(Impersonate::ChromeV146), + "chrome_147" => Ok(Impersonate::ChromeV147), + "chrome_148" => Ok(Impersonate::ChromeV148), "chrome" => Ok(Impersonate::Chrome), // Edge variants "edge_144" => Ok(Impersonate::EdgeV144), "edge_145" => Ok(Impersonate::EdgeV145), "edge_146" => Ok(Impersonate::EdgeV146), + "edge_147" => Ok(Impersonate::EdgeV147), + "edge_148" => Ok(Impersonate::EdgeV148), "edge" => Ok(Impersonate::Edge), // Opera variants "opera_126" => Ok(Impersonate::OperaV126), "opera_127" => Ok(Impersonate::OperaV127), "opera_128" => Ok(Impersonate::OperaV128), "opera_129" => Ok(Impersonate::OperaV129), + "opera_130" => Ok(Impersonate::OperaV130), + "opera_131" => Ok(Impersonate::OperaV131), "opera" => Ok(Impersonate::Opera), // Safari variants "safari_18.5" => Ok(Impersonate::SafariV18_5), diff --git a/crates/primp-python/src/lib.rs b/crates/primp-python/src/lib.rs index fd6f64d..8485e58 100644 --- a/crates/primp-python/src/lib.rs +++ b/crates/primp-python/src/lib.rs @@ -25,6 +25,7 @@ use response::{BytesIterator, LinesIterator, Response, TextIterator}; mod r#async; +mod response_shared; mod traits; use traits::HeadersTraits; diff --git a/crates/primp-python/src/response.rs b/crates/primp-python/src/response.rs index 48aa895..e641a09 100644 --- a/crates/primp-python/src/response.rs +++ b/crates/primp-python/src/response.rs @@ -1,38 +1,16 @@ use std::sync::Arc; -use anyhow::Result; -use bytes::Bytes; use encoding_rs::{Encoding, UTF_8}; -use html2text::{ - from_read, from_read_with_decorator, - render::{RichDecorator, TrivialDecorator}, -}; use pyo3::{ prelude::*, types::{PyBytes, PyDict, PyString}, IntoPyObjectExt, }; -use pythonize::pythonize; -use serde_json::from_slice; use tokio::sync::Mutex as TMutex; use crate::client_builder::IndexMapSSR; -use crate::error::{body_collection_error, convert_reqwest_error, BodyError, PrimpErrorEnum}; -use crate::traits::HeadersTraits; -use crate::utils::extract_encoding; - -/// Collect body bytes from a response using a pre-allocated buffer. -/// Uses `Vec::with_capacity(8 * 1024)` for efficient buffer allocation. -async fn collect_body_bytes(resp: &mut ::primp::Response) -> Result { - let mut buf = Vec::with_capacity(8 * 1024); - loop { - match resp.chunk().await { - Ok(Some(chunk)) => buf.extend_from_slice(&chunk), - Ok(None) => break Ok(Bytes::from(buf)), - Err(e) => return Err(body_collection_error(&e.to_string())), - } - } -} +use crate::error::convert_reqwest_error; +use crate::response_shared; /// A struct representing an HTTP response. /// @@ -94,17 +72,6 @@ impl Response { } } - async fn read_bytes(&self) -> Result { - let resp = Arc::clone(&self.resp); - let mut resp_guard = resp.lock().await; - match resp_guard.as_mut() { - Some(r) => collect_body_bytes(r).await, - None => Err(BodyError::new_err( - "Response body already consumed or moved", - )), - } - } - async fn next_chunk(&self) -> Result>, PyErr> { let resp = Arc::clone(&self.resp); let mut resp_guard = resp.lock().await; @@ -122,50 +89,17 @@ impl Response { #[pymethods] impl Response { #[getter] - fn get_content<'rs>(&mut self, py: Python<'rs>) -> Result> { - if !self.streaming { - if let Some(content) = &self._content { - let cloned = content.clone_ref(py); - return Ok(cloned.into_bound(py)); - } - } - - let runtime = crate::get_runtime(py); - let bytes: Bytes = py.detach(|| runtime.block_on(self.read_bytes()))?; - let content = PyBytes::new(py, &bytes); - - if !self.streaming { - self._content = Some(content.clone().unbind()); - } - Ok(content) + fn get_content<'rs>(&mut self, py: Python<'rs>) -> PyResult> { + response_shared::get_content(&self.resp, &mut self._content, self.streaming, py) } #[getter] - fn get_encoding(&mut self, py: Python<'_>) -> Result { - if let Some(encoding) = self._encoding.as_ref() { - return Ok(encoding.clone()); - } - - let resp = Arc::clone(&self.resp); - let runtime = crate::get_runtime(py); - let encoding = py.detach(|| { - runtime.block_on(async { - let resp_guard = resp.lock().await; - match resp_guard.as_ref() { - Some(r) => Ok(extract_encoding(r.headers()).name().to_string()), - None => Err(BodyError::new_err( - "Response body already consumed or moved", - )), - } - }) - })?; - - self._encoding = Some(encoding.clone()); - Ok(encoding) + fn get_encoding(&mut self, py: Python<'_>) -> PyResult { + response_shared::get_encoding(&self.resp, &mut self._encoding, py) } #[setter] - fn set_encoding(&mut self, encoding: Option) -> Result<()> { + fn set_encoding(&mut self, encoding: Option) -> PyResult<()> { if let Some(encoding) = encoding { self._encoding = Some(encoding); } @@ -173,138 +107,51 @@ impl Response { } #[getter] - fn text<'rs>(&mut self, py: Python<'rs>) -> Result> { - let content = self.get_content(py)?.unbind(); - let encoding = self.get_encoding(py)?; - let raw_bytes = content.as_bytes(py); - let encoding = Encoding::for_label(encoding.as_bytes()).unwrap_or(UTF_8); - let (text, _, _) = encoding.decode(raw_bytes); - Ok(text.into_pyobject_or_pyerr(py)?) + fn text<'rs>(&mut self, py: Python<'rs>) -> PyResult> { + response_shared::text( + &self.resp, + &mut self._content, + &mut self._encoding, + self.streaming, + py, + ) } fn json<'rs>(&mut self, py: Python<'rs>) -> PyResult> { - let content = self.get_content(py)?.unbind(); - let raw_bytes = content.as_bytes(py); - let json_value: serde_json::Value = match from_slice(raw_bytes) { - Ok(v) => v, - Err(e) => { - let json_module = py.import("json")?; - let error_type = json_module.getattr("JSONDecodeError")?; - let doc = String::from_utf8_lossy(raw_bytes).to_string(); - let msg = e.to_string(); - let pos = e.line().saturating_sub(1); - return Err(PyErr::from_value(error_type.call1((&msg, &doc, pos))?)); - } - }; - let result = pythonize(py, &json_value)?; - Ok(result) + response_shared::json(&self.resp, &mut self._content, self.streaming, py) } #[getter] - fn get_headers<'rs>(&mut self, py: Python<'rs>) -> Result> { - if let Some(headers) = &self._headers { - return Ok(headers.clone().into_pyobject(py)?); - } - - let resp = Arc::clone(&self.resp); - let runtime = crate::get_runtime(py); - let new_headers = py.detach(|| { - runtime.block_on(async { - let resp_guard = resp.lock().await; - match resp_guard.as_ref() { - Some(r) => Ok(r.headers().to_indexmap()), - None => Err(BodyError::new_err( - "Response body already consumed or moved", - )), - } - }) - })?; - - let py_dict = new_headers.clone().into_pyobject(py)?; - self._headers = Some(new_headers); - Ok(py_dict) + fn get_headers<'rs>(&mut self, py: Python<'rs>) -> PyResult> { + response_shared::get_headers(&self.resp, &mut self._headers, py) } #[getter] - fn get_cookies<'rs>(&mut self, py: Python<'rs>) -> Result> { - if let Some(cookies) = &self._cookies { - return Ok(cookies.clone().into_pyobject(py)?); - } - - let resp = Arc::clone(&self.resp); - let runtime = crate::get_runtime(py); - let cookie_map = py.detach(|| { - runtime.block_on(async { - let resp_guard = resp.lock().await; - match resp_guard.as_ref() { - Some(r) => Ok(crate::extract_cookies_to_indexmap(r.headers())), - None => Err(BodyError::new_err( - "Response body already consumed or moved", - )), - } - }) - })?; - - let py_dict = cookie_map.clone().into_pyobject(py)?; - self._cookies = Some(cookie_map); - Ok(py_dict) + fn get_cookies<'rs>(&mut self, py: Python<'rs>) -> PyResult> { + response_shared::get_cookies(&self.resp, &mut self._cookies, py) } #[getter] - fn text_markdown(&mut self, py: Python) -> Result { - let content = self.get_content(py)?.unbind(); - let raw_bytes = content.as_bytes(py); - let text = py.detach(|| from_read(raw_bytes, 100))?; - Ok(text) + fn text_markdown(&mut self, py: Python<'_>) -> PyResult { + response_shared::text_markdown(&self.resp, &mut self._content, self.streaming, py) } #[getter] - fn text_plain(&mut self, py: Python) -> Result { - let content = self.get_content(py)?.unbind(); - let raw_bytes = content.as_bytes(py); - let text = - py.detach(|| from_read_with_decorator(raw_bytes, 100, TrivialDecorator::new()))?; - Ok(text) + fn text_plain(&mut self, py: Python<'_>) -> PyResult { + response_shared::text_plain(&self.resp, &mut self._content, self.streaming, py) } #[getter] - fn text_rich(&mut self, py: Python) -> Result { - let content = self.get_content(py)?.unbind(); - let raw_bytes = content.as_bytes(py); - let text = py.detach(|| from_read_with_decorator(raw_bytes, 100, RichDecorator::new()))?; - Ok(text) + fn text_rich(&mut self, py: Python<'_>) -> PyResult { + response_shared::text_rich(&self.resp, &mut self._content, self.streaming, py) } fn raise_for_status(&self) -> PyResult<()> { - if self.status_code >= 400 { - let reason = if self.status_code < 600 { - match self.status_code { - 400 => "Bad Request", - 401 => "Unauthorized", - 403 => "Forbidden", - 404 => "Not Found", - 405 => "Method Not Allowed", - 409 => "Conflict", - 500 => "Internal Server Error", - 502 => "Bad Gateway", - 503 => "Service Unavailable", - _ => "Error", - } - } else { - "Unknown Error" - }; - - return Err(PyErr::from(PrimpErrorEnum::HttpStatus( - self.status_code, - reason.to_string(), - self.url.clone(), - ))); - } - Ok(()) + response_shared::raise_for_status(self.status_code, &self.url) } fn read<'rs>(&mut self, py: Python<'rs>) -> PyResult> { - Ok(self.get_content(py)?) + response_shared::get_content(&self.resp, &mut self._content, self.streaming, py) } #[pyo3(signature = (chunk_size=None))] diff --git a/crates/primp-python/src/response_shared.rs b/crates/primp-python/src/response_shared.rs new file mode 100644 index 0000000..76e2095 --- /dev/null +++ b/crates/primp-python/src/response_shared.rs @@ -0,0 +1,275 @@ +use std::sync::Arc; + +use bytes::Bytes; +use encoding_rs::{Encoding, UTF_8}; +use pyo3::{ + prelude::*, + types::{PyBytes, PyDict, PyString}, + IntoPyObjectExt, +}; +use pythonize::pythonize; +use serde_json::from_slice; +use tokio::sync::Mutex as TMutex; + +use crate::client_builder::IndexMapSSR; +use crate::error::{body_collection_error, BodyError, PrimpErrorEnum}; +use crate::traits::HeadersTraits; +use crate::utils::extract_encoding; + +/// Collect body bytes from a response using a pre-allocated buffer. +pub async fn collect_body_bytes(resp: &mut ::primp::Response) -> Result { + let mut buf = Vec::with_capacity(8 * 1024); + loop { + match resp.chunk().await { + Ok(Some(chunk)) => buf.extend_from_slice(&chunk), + Ok(None) => break Ok(Bytes::from(buf)), + Err(e) => return Err(body_collection_error(&e.to_string())), + } + } +} + +/// Raise HTTPError for 4xx/5xx status codes. +pub fn raise_for_status(status_code: u16, url: &str) -> PyResult<()> { + if status_code >= 400 { + let reason = if status_code < 600 { + match status_code { + 400 => "Bad Request", + 401 => "Unauthorized", + 403 => "Forbidden", + 404 => "Not Found", + 405 => "Method Not Allowed", + 409 => "Conflict", + 500 => "Internal Server Error", + 502 => "Bad Gateway", + 503 => "Service Unavailable", + _ => "Error", + } + } else { + "Unknown Error" + }; + return Err(PyErr::from(PrimpErrorEnum::HttpStatus( + status_code, + reason.to_string(), + url.to_string(), + ))); + } + Ok(()) +} + +/// Read response body bytes, blocking on the Tokio runtime. +pub fn read_body_bytes<'py>( + resp: &Arc>>, + py: Python<'py>, +) -> PyResult { + let r = Arc::clone(resp); + let runtime = crate::get_runtime(py); + py.detach(|| { + runtime.block_on(async { + let mut guard = r.lock().await; + match guard.as_mut() { + Some(r) => collect_body_bytes(r).await, + None => Err(BodyError::new_err( + "Response body already consumed or moved", + )), + } + }) + }) +} + +/// Get response content as bytes, using cache for non-streaming. +pub fn get_content<'py>( + resp: &Arc>>, + content_cache: &mut Option>, + streaming: bool, + py: Python<'py>, +) -> PyResult> { + if !streaming { + if let Some(content) = content_cache { + return Ok(content.clone_ref(py).into_bound(py)); + } + } + + let bytes: Bytes = read_body_bytes(resp, py)?; + let content = PyBytes::new(py, &bytes); + + if !streaming { + *content_cache = Some(content.clone().unbind()); + } + Ok(content) +} + +/// Get character encoding from response headers or cache. +pub fn get_encoding<'py>( + resp: &Arc>>, + encoding_cache: &mut Option, + py: Python<'py>, +) -> PyResult { + if let Some(encoding) = encoding_cache.as_ref() { + return Ok(encoding.clone()); + } + + let r = Arc::clone(resp); + let runtime = crate::get_runtime(py); + let encoding: String = py.detach(|| { + runtime.block_on(async { + let guard = r.lock().await; + match guard.as_ref() { + Some(r) => Ok(extract_encoding(r.headers()).name().to_string()), + None => Err(BodyError::new_err( + "Response body already consumed or moved", + )), + } + }) + })?; + + *encoding_cache = Some(encoding.clone()); + Ok(encoding) +} + +/// Get response text content. +pub fn text<'py>( + resp: &Arc>>, + content_cache: &mut Option>, + encoding_cache: &mut Option, + streaming: bool, + py: Python<'py>, +) -> PyResult> { + let content = get_content(resp, content_cache, streaming, py)?; + let enc = get_encoding(resp, encoding_cache, py)?; + let raw_bytes = content.as_bytes(); + let encoding = Encoding::for_label(enc.as_bytes()).unwrap_or(UTF_8); + let (text, _, _) = encoding.decode(raw_bytes); + text.into_pyobject_or_pyerr(py) +} + +/// Parse response body as JSON. +pub fn json<'py>( + resp: &Arc>>, + content_cache: &mut Option>, + streaming: bool, + py: Python<'py>, +) -> PyResult> { + let content = get_content(resp, content_cache, streaming, py)?.unbind(); + let raw_bytes = content.as_bytes(py); + let json_value: serde_json::Value = match from_slice(raw_bytes) { + Ok(v) => v, + Err(e) => { + let json_module = py.import("json")?; + let error_type = json_module.getattr("JSONDecodeError")?; + let doc = String::from_utf8_lossy(raw_bytes).to_string(); + let msg = e.to_string(); + let pos = e.line().saturating_sub(1); + return Err(PyErr::from_value(error_type.call1((&msg, &doc, pos))?)); + } + }; + let result = pythonize(py, &json_value)?; + Ok(result) +} + +/// Get response headers from cache or response. +pub fn get_headers<'py>( + resp: &Arc>>, + headers_cache: &mut Option, + py: Python<'py>, +) -> PyResult> { + if let Some(headers) = headers_cache { + return headers.clone().into_pyobject(py); + } + + let r = Arc::clone(resp); + let runtime = crate::get_runtime(py); + let headers: IndexMapSSR = py.detach(|| { + runtime.block_on(async { + let guard = r.lock().await; + match guard.as_ref() { + Some(r) => Ok(r.headers().to_indexmap()), + None => Err(BodyError::new_err( + "Response body already consumed or moved", + )), + } + }) + })?; + + let py_dict = headers.clone().into_pyobject(py)?; + *headers_cache = Some(headers); + Ok(py_dict) +} + +/// Get response cookies from cache or response. +pub fn get_cookies<'py>( + resp: &Arc>>, + cookies_cache: &mut Option, + py: Python<'py>, +) -> PyResult> { + if let Some(cookies) = cookies_cache { + return cookies.clone().into_pyobject(py); + } + + let r = Arc::clone(resp); + let runtime = crate::get_runtime(py); + let cookies: IndexMapSSR = py.detach(|| { + runtime.block_on(async { + let guard = r.lock().await; + match guard.as_ref() { + Some(r) => Ok(crate::extract_cookies_to_indexmap(r.headers())), + None => Err(BodyError::new_err( + "Response body already consumed or moved", + )), + } + }) + })?; + + let py_dict = cookies.clone().into_pyobject(py)?; + *cookies_cache = Some(cookies); + Ok(py_dict) +} + +/// Get HTML converted to Markdown. +pub fn text_markdown<'py>( + resp: &Arc>>, + content_cache: &mut Option>, + streaming: bool, + py: Python<'py>, +) -> PyResult { + let content = get_content(resp, content_cache, streaming, py)?; + let raw_bytes = content.as_bytes(); + let text = py.detach(|| { + html2text::from_read(raw_bytes, 100) + .map_err(|e| PyErr::new::(e.to_string())) + })?; + Ok(text) +} + +/// Get HTML converted to plain text. +pub fn text_plain<'py>( + resp: &Arc>>, + content_cache: &mut Option>, + streaming: bool, + py: Python<'py>, +) -> PyResult { + use html2text::{from_read_with_decorator, render::TrivialDecorator}; + let content = get_content(resp, content_cache, streaming, py)?; + let raw_bytes = content.as_bytes(); + let text = py.detach(|| { + from_read_with_decorator(raw_bytes, 100, TrivialDecorator::new()) + .map_err(|e| PyErr::new::(e.to_string())) + })?; + Ok(text) +} + +/// Get HTML converted to rich text. +pub fn text_rich<'py>( + resp: &Arc>>, + content_cache: &mut Option>, + streaming: bool, + py: Python<'py>, +) -> PyResult { + use html2text::{from_read_with_decorator, render::RichDecorator}; + let content = get_content(resp, content_cache, streaming, py)?; + let raw_bytes = content.as_bytes(); + let text = py.detach(|| { + from_read_with_decorator(raw_bytes, 100, RichDecorator::new()) + .map_err(|e| PyErr::new::(e.to_string())) + })?; + Ok(text) +} diff --git a/crates/primp-python/src/traits.rs b/crates/primp-python/src/traits.rs index 43e10d6..4afd3f0 100644 --- a/crates/primp-python/src/traits.rs +++ b/crates/primp-python/src/traits.rs @@ -1,12 +1,10 @@ use foldhash::fast::RandomState; -use indexmap::IndexMap; use ::primp::header::{HeaderMap, HeaderName, HeaderValue}; +use crate::client_builder::IndexMapSSR; use crate::error::PrimpErrorEnum; -type IndexMapSSR = IndexMap; - /// Try to convert a key-value pair to header name and value. /// Returns an error if either conversion fails. fn try_make_header(key: &str, value: &str) -> Result<(HeaderName, HeaderValue), PrimpErrorEnum> { diff --git a/crates/primp-reqwest/src/async_impl/client.rs b/crates/primp-reqwest/src/async_impl/client.rs index 16aa393..da17e16 100644 --- a/crates/primp-reqwest/src/async_impl/client.rs +++ b/crates/primp-reqwest/src/async_impl/client.rs @@ -242,6 +242,8 @@ struct Config { http2_headers_order: Option>, #[cfg(feature = "http2")] http2_initial_stream_id: Option, + #[cfg(feature = "http2")] + http2_initial_stream_window_size_increment: Option, local_address: Option, #[cfg(any( target_os = "android", @@ -383,9 +385,12 @@ impl ClientBuilder { http2_headers_pseudo_order: None, #[cfg(feature = "http2")] http2_headers_priority: None, + #[cfg(feature = "http2")] http2_headers_order: None, #[cfg(feature = "http2")] http2_initial_stream_id: None, + #[cfg(feature = "http2")] + http2_initial_stream_window_size_increment: None, local_address: None, #[cfg(any( target_os = "android", @@ -1008,6 +1013,9 @@ impl ClientBuilder { if let Some(stream_id) = config.http2_initial_stream_id { builder.http2_initial_stream_id(stream_id); } + if let Some(incr) = config.http2_initial_stream_window_size_increment { + builder.http2_initial_stream_window_size_increment(incr); + } } builder.timer(hyper_util::rt::TokioTimer::new()); @@ -1790,6 +1798,22 @@ impl ClientBuilder { self } + /// Sets extra receive window capacity to add to new locally-initiated streams. + /// + /// When set, after creating a new stream, a WINDOW_UPDATE frame will be sent + /// to increase the stream's receive window by this amount. + /// This is used for browser fingerprinting (e.g. Firefox adds 12451840 + /// to the first stream's receive window). + #[cfg(feature = "http2")] + #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] + pub fn http2_initial_stream_window_size_increment( + mut self, + sz: impl Into>, + ) -> ClientBuilder { + self.config.http2_initial_stream_window_size_increment = sz.into(); + self + } + // TCP options /// Set whether sockets have `TCP_NODELAY` enabled. @@ -3292,8 +3316,8 @@ impl fmt::Debug for Pending { } #[cfg(test)] +#[cfg(not(feature = "rustls-no-provider"))] mod tests { - #![cfg(not(feature = "rustls-no-provider"))] #[tokio::test] async fn execute_request_rejects_invalid_urls() { diff --git a/crates/primp-reqwest/src/async_impl/request.rs b/crates/primp-reqwest/src/async_impl/request.rs index 5f8a4ed..9589024 100644 --- a/crates/primp-reqwest/src/async_impl/request.rs +++ b/crates/primp-reqwest/src/async_impl/request.rs @@ -684,8 +684,8 @@ impl TryFrom for HttpRequest { } #[cfg(test)] +#[cfg(not(feature = "rustls-no-provider"))] mod tests { - #![cfg(not(feature = "rustls-no-provider"))] use super::*; #[cfg(feature = "query")] diff --git a/crates/primp-reqwest/src/error.rs b/crates/primp-reqwest/src/error.rs index 57df307..bfbd213 100644 --- a/crates/primp-reqwest/src/error.rs +++ b/crates/primp-reqwest/src/error.rs @@ -440,7 +440,7 @@ mod tests { #[test] fn from_unknown_io_error() { - let orig = io::Error::new(io::ErrorKind::Other, "orly"); + let orig = io::Error::other("orly"); let err = super::decode_io(orig); match err.inner.kind { Kind::Decode => (), diff --git a/crates/primp-rustls/rustls/src/client/hs.rs b/crates/primp-rustls/rustls/src/client/hs.rs index a85d13c..17653ad 100644 --- a/crates/primp-rustls/rustls/src/client/hs.rs +++ b/crates/primp-rustls/rustls/src/client/hs.rs @@ -27,7 +27,9 @@ use crate::enums::{ use crate::error::{Error, PeerIncompatible, PeerMisbehaved}; use crate::hash_hs::HandshakeHashBuffer; use crate::log::{debug, trace}; -use crate::msgs::base::{Payload, PayloadU16, PayloadU8}; +use crate::msgs::base::Payload; +#[cfg(feature = "impersonate")] +use crate::msgs::base::{PayloadU16, PayloadU8}; use crate::msgs::enums::{Compression, ExtensionType}; use crate::msgs::handshake::{ CertificateStatusRequest, ClientExtensions, ClientExtensionsInput, ClientHelloPayload, @@ -51,10 +53,12 @@ use crate::SupportedCipherSuite; /// /// Placeholder for the first/trailing GREASE extension type. /// Must match the value used in contiguous_extensions and unknown_extensions. +#[cfg(feature = "impersonate")] const GREASE_EXT_FIRST_PLACEHOLDER: u16 = 0x6a6a; /// Placeholder for the last GREASE extension type. /// Must match the value used in contiguous_extensions and unknown_extensions. +#[cfg(feature = "impersonate")] const GREASE_EXT_LAST_PLACEHOLDER: u16 = 0x0a0a; #[cfg(feature = "impersonate")] @@ -256,10 +260,16 @@ fn emit_client_hello_for_retry( // builder semantics. let forbids_tls12 = cx.common.is_quic() || ech_state.is_some(); + #[cfg(not(feature = "impersonate"))] + let supported_versions = SupportedProtocolVersions { + tls12: config.supports_version(ProtocolVersion::TLSv1_2) && !forbids_tls12, + tls13: config.supports_version(ProtocolVersion::TLSv1_3), + }; + + #[cfg(feature = "impersonate")] let mut supported_versions = SupportedProtocolVersions { tls12: config.supports_version(ProtocolVersion::TLSv1_2) && !forbids_tls12, tls13: config.supports_version(ProtocolVersion::TLSv1_3), - #[cfg(feature = "impersonate")] grease: false, }; @@ -296,7 +306,7 @@ fn emit_client_hello_for_retry( }); #[cfg(not(feature = "impersonate"))] - let mut named_groups_vec: Vec = config + let named_groups_vec: Vec = config .provider .kx_groups .iter() @@ -532,6 +542,7 @@ fn emit_client_hello_for_retry( // see if we can send a second KeyShare for "free". We only do this if the same // algorithm is also supported separately by our provider for this version // (`find_kx_group` looks that up). + #[cfg(feature = "impersonate")] let hybrid_group = key_share.hybrid_component().map(|(g, _)| g); if let Some((component_group, component_share)) = key_share.hybrid_component().filter(|(group, _)| { diff --git a/crates/primp-tokio-rustls/src/common/test_stream.rs b/crates/primp-tokio-rustls/src/common/test_stream.rs index e0ed818..f8d022d 100644 --- a/crates/primp-tokio-rustls/src/common/test_stream.rs +++ b/crates/primp-tokio-rustls/src/common/test_stream.rs @@ -196,7 +196,7 @@ async fn stream_good_impl(vectored: bool, bufread: bool) -> io::Result<()> { } let mut buf = String::new(); - dbg!(server.process_new_packets()).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + dbg!(server.process_new_packets()).map_err(io::Error::other)?; dbg!(server.reader().read_to_string(&mut buf))?; assert_eq!(buf, "Hello World!"); diff --git a/crates/primp/README.md b/crates/primp/README.md index 1926522..85f2403 100644 --- a/crates/primp/README.md +++ b/crates/primp/README.md @@ -52,11 +52,11 @@ cargo test --workspace | Browser | Profiles | |:--------|:---------| -| Chrome | `chrome_144`, `chrome_145`, `chrome_146`, `chrome` | +| Chrome | `chrome_144`, `chrome_145`, `chrome_146`, `chrome_147`, `chrome_148`, `chrome` | | Safari | `safari_18.5`, `safari_26`, `safari_26.3`, `safari` | -| Edge | `edge_144`, `edge_145`, `edge_146`, `edge` | +| Edge | `edge_144`, `edge_145`, `edge_146`, `edge_147`, `edge_148`, `edge` | | Firefox | `firefox_140`, `firefox_146`, `firefox_147`, `firefox_148`, `firefox` | -| Opera | `opera_126`, `opera_127`, `opera_128`, `opera_129`, `opera` | +| Opera | `opera_126`, `opera_127`, `opera_128`, `opera_129`, `opera_130`, `opera_131`, `opera` | | Random | `random` | **OS:** `android`, `ios`, `linux`, `macos`, `windows`, `random` diff --git a/crates/primp/src/imp/chrome/mod.rs b/crates/primp/src/imp/chrome/mod.rs index 41f7acd..0901ccc 100644 --- a/crates/primp/src/imp/chrome/mod.rs +++ b/crates/primp/src/imp/chrome/mod.rs @@ -21,26 +21,23 @@ //! ``` pub use crate::imp::Impersonate; +#[cfg(feature = "http2")] +use crate::imp::{PseudoId, PseudoOrder, SettingId, SettingsOrder}; +use http::header::*; +use rustls::client::{BrowserEmulator, BrowserType, BrowserVersion}; +use rustls::crypto::emulation; +use std::sync::{Arc, OnceLock}; /// Builds browser settings for a specific Chrome version and OS. pub(crate) fn build_chrome_settings( chrome: Impersonate, os: crate::imp::ImpersonateOS, ) -> crate::imp::BrowserSettings { - use http::header::*; - use rustls::client::{BrowserEmulator, BrowserType, BrowserVersion}; - use rustls::crypto::emulation; - use std::sync::OnceLock; - let user_agent = build_user_agent(chrome, os); let sec_ch_ua = build_sec_ch_ua(chrome, os); - let mut headers = http::HeaderMap::with_capacity(13); + let mut headers = base_chrome_headers().clone(); headers.insert(USER_AGENT, http::HeaderValue::from_static(user_agent)); - headers.insert( - "upgrade-insecure-requests", - http::HeaderValue::from_static("1"), - ); headers.insert("sec-ch-ua", http::HeaderValue::from_static(sec_ch_ua)); headers.insert( "sec-ch-ua-mobile", @@ -57,72 +54,11 @@ pub(crate) fn build_chrome_settings( ); headers.insert( "sec-ch-ua-platform", - http::HeaderValue::from_static(os_platform(os)), + http::HeaderValue::from_static(crate::imp::os_platform(os)), ); - headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); - headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); - headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); - headers.insert("sec-fetch-user", http::HeaderValue::from_static("?1")); - headers.insert(ACCEPT, http::HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")); - headers.insert( - "accept-encoding", - http::HeaderValue::from_static("gzip, deflate, br, zstd"), - ); - headers.insert( - "accept-language", - http::HeaderValue::from_static("en-US,en;q=0.9"), - ); - headers.insert("priority", http::HeaderValue::from_static("u=0, i")); // Get cached browser emulator for Chrome (avoids Vec allocations on each call) - let browser_emulator = match chrome { - Impersonate::ChromeV144 => { - static CHROME_144: OnceLock = OnceLock::new(); - CHROME_144 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Chrome, BrowserVersion::new(144, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::CHROME.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::CHROME.to_vec()); - emulator.named_groups = Some(emulation::named_groups::CHROME.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::CHROME); - emulator - }) - .clone() - } - Impersonate::ChromeV145 => { - static CHROME_145: OnceLock = OnceLock::new(); - CHROME_145 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Chrome, BrowserVersion::new(145, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::CHROME.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::CHROME.to_vec()); - emulator.named_groups = Some(emulation::named_groups::CHROME.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::CHROME); - emulator - }) - .clone() - } - Impersonate::ChromeV146 => { - static CHROME_146: OnceLock = OnceLock::new(); - CHROME_146 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Chrome, BrowserVersion::new(146, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::CHROME.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::CHROME.to_vec()); - emulator.named_groups = Some(emulation::named_groups::CHROME.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::CHROME); - emulator - }) - .clone() - } - _ => unreachable!(), - }; + let browser_emulator = chrome_emulator(chrome); let http2 = build_http2_settings(chrome); @@ -164,6 +100,22 @@ fn build_user_agent(chrome: Impersonate, os: crate::imp::ImpersonateOS) -> &'sta crate::imp::ImpersonateOS::IOS => "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/146.0.0.0 Mobile/15E148 Safari/604.1", _ => build_user_agent(chrome, crate::imp::random_impersonate_os()), }, + Impersonate::ChromeV147 => match os { + crate::imp::ImpersonateOS::Windows => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + crate::imp::ImpersonateOS::MacOS => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + crate::imp::ImpersonateOS::Linux => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + crate::imp::ImpersonateOS::Android => "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36", + crate::imp::ImpersonateOS::IOS => "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/147.0.0.0 Mobile/15E148 Safari/604.1", + _ => build_user_agent(chrome, crate::imp::random_impersonate_os()), + }, + Impersonate::ChromeV148 => match os { + crate::imp::ImpersonateOS::Windows => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", + crate::imp::ImpersonateOS::MacOS => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", + crate::imp::ImpersonateOS::Linux => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", + crate::imp::ImpersonateOS::Android => "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Mobile Safari/537.36", + crate::imp::ImpersonateOS::IOS => "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/148.0.0.0 Mobile/15E148 Safari/604.1", + _ => build_user_agent(chrome, crate::imp::random_impersonate_os()), + }, _ => unreachable!(), } } @@ -171,80 +123,129 @@ fn build_user_agent(chrome: Impersonate, os: crate::imp::ImpersonateOS) -> &'sta /// Builds a sec-ch-ua header value for a Chrome version and OS. fn build_sec_ch_ua(chrome: Impersonate, _os: crate::imp::ImpersonateOS) -> &'static str { match chrome { - Impersonate::ChromeV144 => r#""Not-A.Brand";v="24", "Chromium";v="144""#, - Impersonate::ChromeV145 => r#""Not-A.Brand";v="24", "Chromium";v="145""#, - Impersonate::ChromeV146 => r#""Not-A.Brand";v="24", "Chromium";v="146""#, + Impersonate::ChromeV144 => { + r#""Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144""# + } + Impersonate::ChromeV145 => { + r#""Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145""# + } + Impersonate::ChromeV146 => { + r#""Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146""# + } + Impersonate::ChromeV147 => { + r#""Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147""# + } + Impersonate::ChromeV148 => { + r#""Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99""# + } _ => unreachable!(), } } -/// Returns a platform string for sec-ch-ua-platform header. -fn os_platform(os: crate::imp::ImpersonateOS) -> &'static str { - match os { - crate::imp::ImpersonateOS::Windows => r#""Windows""#, - crate::imp::ImpersonateOS::MacOS => r#""macOS""#, - crate::imp::ImpersonateOS::Linux => r#""Linux""#, - crate::imp::ImpersonateOS::Android => r#""Android""#, - crate::imp::ImpersonateOS::IOS => r#""iOS""#, - _ => os_platform(crate::imp::random_impersonate_os()), +/// Builds HTTP/2 settings for a Chrome version. +#[cfg(feature = "http2")] +fn build_http2_settings(chrome: Impersonate) -> crate::imp::Http2Data { + // Chrome 148 uses different header order (sec-ch-ua after sec-fetch-*) + let headers_order = if matches!(chrome, Impersonate::ChromeV148) { + Some(crate::imp::header_order_upgrade_first_sec_chua_last().clone()) + } else { + Some(crate::imp::header_order_sec_chua_first().clone()) + }; + + crate::imp::Http2Data { + settings_order: Some(chrome_settings_order().clone()), + headers_pseudo_order: Some(chrome_pseudo_order().clone()), + headers_order, + headers_priority: Some((255, 0, true)), + initial_stream_window_size: Some(crate::imp::CHROME_INITIAL_STREAM_WINDOW), + initial_connection_window_size: Some(crate::imp::CHROME_INITIAL_CONNECTION_WINDOW), + max_header_list_size: Some(crate::imp::CHROME_MAX_HEADER_LIST_SIZE), + header_table_size: Some(crate::imp::CHROME_HEADER_TABLE_SIZE), + ..Default::default() + } +} + +fn chrome_emulator(chrome: Impersonate) -> Arc { + match chrome { + Impersonate::ChromeV144 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_chrome_emulator(144))) + .clone() + } + Impersonate::ChromeV145 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_chrome_emulator(145))) + .clone() + } + Impersonate::ChromeV146 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_chrome_emulator(146))) + .clone() + } + Impersonate::ChromeV147 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_chrome_emulator(147))) + .clone() + } + Impersonate::ChromeV148 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_chrome_emulator(148))) + .clone() + } + _ => unreachable!(), } } -/// Builds HTTP/2 settings for a Chrome version. -#[cfg(feature = "http2")] -fn build_http2_settings(_chrome: Impersonate) -> crate::imp::Http2Data { - use crate::imp::{PseudoId, PseudoOrder, SettingId, SettingsOrder}; +fn new_chrome_emulator(major: u16) -> BrowserEmulator { + let mut emulator = BrowserEmulator::new(BrowserType::Chrome, BrowserVersion::new(major, 0, 0)); + emulator.cipher_suites = Some(emulation::cipher_suites::CHROME.to_vec()); + emulator.signature_algorithms = Some(emulation::signature_algorithms::CHROME.to_vec()); + emulator.named_groups = Some(emulation::named_groups::CHROME.to_vec()); + emulator.extension_order_seed = Some(emulation::extension_order::CHROME); + emulator +} + +fn base_chrome_headers() -> &'static http::HeaderMap { + static BASE: OnceLock = OnceLock::new(); + BASE.get_or_init(|| { + let mut headers = http::HeaderMap::with_capacity(13); + headers.insert(ACCEPT, http::HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")); + headers.insert("accept-encoding", http::HeaderValue::from_static("gzip, deflate, br, zstd")); + headers.insert("accept-language", http::HeaderValue::from_static("en-US,en;q=0.9")); + headers.insert("upgrade-insecure-requests", http::HeaderValue::from_static("1")); + headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); + headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); + headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); + headers.insert("sec-fetch-user", http::HeaderValue::from_static("?1")); + headers.insert("priority", http::HeaderValue::from_static("u=0, i")); + headers + }) +} - // Chrome 144 sends only settings: 1, 2, 4, 6 - // Expected fingerprint: 1:65536;2:0;4:6291456;6:262144 - let settings_order = Some( +#[cfg(feature = "http2")] +fn chrome_settings_order() -> &'static SettingsOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { SettingsOrder::builder() - .push(SettingId::HeaderTableSize) // 1: 65536 - .push(SettingId::EnablePush) // 2: 0 - .push(SettingId::InitialWindowSize) // 4: 6291456 - .push(SettingId::MaxHeaderListSize) // 6: 262144 - .build_without_extend(), - ); + .push(SettingId::HeaderTableSize) + .push(SettingId::EnablePush) + .push(SettingId::InitialWindowSize) + .push(SettingId::MaxHeaderListSize) + .build_without_extend() + }) +} - let headers_pseudo_order = Some( +#[cfg(feature = "http2")] +fn chrome_pseudo_order() -> &'static PseudoOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { PseudoOrder::builder() .push(PseudoId::Method) .push(PseudoId::Authority) .push(PseudoId::Scheme) .push(PseudoId::Path) - .build(), - ); - - crate::imp::Http2Data { - initial_stream_window_size: Some(6291456), - initial_connection_window_size: Some(15728640), - max_concurrent_streams: None, - max_frame_size: None, - max_header_list_size: Some(262144), - header_table_size: Some(65536), - enable_push: Some(false), - enable_connect_protocol: None, - no_rfc7540_priorities: None, - settings_order, - headers_pseudo_order, - headers_priority: Some((255, 0, true)), // Chrome: weight=255 (displays as 0 on wire after -1), dep=0, excl=1 - headers_order: Some(vec![ - http::HeaderName::from_static("sec-ch-ua"), - http::HeaderName::from_static("sec-ch-ua-mobile"), - http::HeaderName::from_static("sec-ch-ua-platform"), - http::HeaderName::from_static("upgrade-insecure-requests"), - http::HeaderName::from_static("user-agent"), - http::HeaderName::from_static("accept"), - http::HeaderName::from_static("sec-fetch-site"), - http::HeaderName::from_static("sec-fetch-mode"), - http::HeaderName::from_static("sec-fetch-user"), - http::HeaderName::from_static("sec-fetch-dest"), - http::HeaderName::from_static("accept-encoding"), - http::HeaderName::from_static("accept-language"), - http::HeaderName::from_static("priority"), - ]), - initial_stream_id: None, - } + .build() + }) } #[cfg(test)] diff --git a/crates/primp/src/imp/edge/mod.rs b/crates/primp/src/imp/edge/mod.rs index 2d7996c..23e7c57 100644 --- a/crates/primp/src/imp/edge/mod.rs +++ b/crates/primp/src/imp/edge/mod.rs @@ -22,26 +22,23 @@ //! ``` pub use crate::imp::Impersonate; +#[cfg(feature = "http2")] +use crate::imp::{PseudoId, PseudoOrder, SettingId, SettingsOrder}; +use http::header::*; +use rustls::client::{BrowserEmulator, BrowserType, BrowserVersion}; +use rustls::crypto::emulation; +use std::sync::{Arc, OnceLock}; /// Builds browser settings for a specific Edge version and OS. pub(crate) fn build_edge_settings( edge: Impersonate, os: crate::imp::ImpersonateOS, ) -> crate::imp::BrowserSettings { - use http::header::*; - use rustls::client::{BrowserEmulator, BrowserType, BrowserVersion}; - use rustls::crypto::emulation; - use std::sync::OnceLock; - let user_agent = build_user_agent(edge, os); let sec_ch_ua = build_sec_ch_ua(edge, os); - let mut headers = http::HeaderMap::with_capacity(13); + let mut headers = base_edge_headers().clone(); headers.insert(USER_AGENT, http::HeaderValue::from_static(user_agent)); - headers.insert( - "upgrade-insecure-requests", - http::HeaderValue::from_static("1"), - ); headers.insert("sec-ch-ua", http::HeaderValue::from_static(sec_ch_ua)); headers.insert( "sec-ch-ua-mobile", @@ -58,72 +55,11 @@ pub(crate) fn build_edge_settings( ); headers.insert( "sec-ch-ua-platform", - http::HeaderValue::from_static(os_platform(os)), - ); - headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); - headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); - headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); - headers.insert("sec-fetch-user", http::HeaderValue::from_static("?1")); - headers.insert(ACCEPT, http::HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")); - headers.insert( - "accept-encoding", - http::HeaderValue::from_static("gzip, deflate, br, zstd"), - ); - headers.insert( - "accept-language", - http::HeaderValue::from_static("en-US,en;q=0.9"), + http::HeaderValue::from_static(crate::imp::os_platform(os)), ); - headers.insert("priority", http::HeaderValue::from_static("u=0, i")); // Get cached browser emulator for Edge (avoids Vec allocations on each call) - let browser_emulator = match edge { - Impersonate::EdgeV144 => { - static EDGE_144: OnceLock = OnceLock::new(); - EDGE_144 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Edge, BrowserVersion::new(144, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::EDGE.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::EDGE.to_vec()); - emulator.named_groups = Some(emulation::named_groups::EDGE.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::EDGE); - emulator - }) - .clone() - } - Impersonate::EdgeV145 => { - static EDGE_145: OnceLock = OnceLock::new(); - EDGE_145 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Edge, BrowserVersion::new(145, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::EDGE.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::EDGE.to_vec()); - emulator.named_groups = Some(emulation::named_groups::EDGE.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::EDGE); - emulator - }) - .clone() - } - Impersonate::EdgeV146 => { - static EDGE_146: OnceLock = OnceLock::new(); - EDGE_146 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Edge, BrowserVersion::new(146, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::EDGE.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::EDGE.to_vec()); - emulator.named_groups = Some(emulation::named_groups::EDGE.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::EDGE); - emulator - }) - .clone() - } - _ => unreachable!(), - }; + let browser_emulator = edge_emulator(edge); let http2 = build_http2_settings(edge); @@ -165,6 +101,22 @@ fn build_user_agent(edge: Impersonate, os: crate::imp::ImpersonateOS) -> &'stati crate::imp::ImpersonateOS::IOS => "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 EdgiOS/146.0.0.0 Mobile/15E148 Safari/605.1.15", _ => build_user_agent(edge, crate::imp::random_impersonate_os()), }, + Impersonate::EdgeV147 => match os { + crate::imp::ImpersonateOS::Windows => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.3912.51", + crate::imp::ImpersonateOS::MacOS => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.3912.51", + crate::imp::ImpersonateOS::Linux => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.3912.51", + crate::imp::ImpersonateOS::Android => "Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36 EdgA/147.0.3912.51", + crate::imp::ImpersonateOS::IOS => "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 EdgiOS/147.0.3912.51 Mobile/15E148 Safari/605.1.15", + _ => build_user_agent(edge, crate::imp::random_impersonate_os()), + }, + Impersonate::EdgeV148 => match os { + crate::imp::ImpersonateOS::Windows => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0", + crate::imp::ImpersonateOS::MacOS => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0", + crate::imp::ImpersonateOS::Linux => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0", + crate::imp::ImpersonateOS::Android => "Mozilla/5.0 (Linux; Android 10; Pixel 3 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Mobile Safari/537.36 EdgA/148.0.0.0", + crate::imp::ImpersonateOS::IOS => "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 EdgiOS/148.0.0.0 Mobile/15E148 Safari/605.1.15", + _ => build_user_agent(edge, crate::imp::random_impersonate_os()), + }, _ => unreachable!(), } } @@ -173,85 +125,126 @@ fn build_user_agent(edge: Impersonate, os: crate::imp::ImpersonateOS) -> &'stati fn build_sec_ch_ua(edge: Impersonate, _os: crate::imp::ImpersonateOS) -> &'static str { match edge { Impersonate::EdgeV144 => { - r#""Chromium";v="144", "Not-A.Brand";v="24", "Microsoft Edge";v="144""# + r#""Not(A:Brand";v="8", "Chromium";v="144", "Microsoft Edge";v="144""# } Impersonate::EdgeV145 => { - r#""Chromium";v="145", "Not-A.Brand";v="24", "Microsoft Edge";v="145""# + r#""Not:A-Brand";v="99", "Microsoft Edge";v="145", "Chromium";v="145""# } Impersonate::EdgeV146 => { r#""Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146""# } + Impersonate::EdgeV147 => { + r#""Microsoft Edge";v="147", "Not.A/Brand";v="8", "Chromium";v="147""# + } + Impersonate::EdgeV148 => { + r#""Chromium";v="148", "Microsoft Edge";v="148", "Not/A)Brand";v="99""# + } _ => unreachable!(), } } -/// Returns a platform string for sec-ch-ua-platform header. -fn os_platform(os: crate::imp::ImpersonateOS) -> &'static str { - match os { - crate::imp::ImpersonateOS::Windows => r#""Windows""#, - crate::imp::ImpersonateOS::MacOS => r#""macOS""#, - crate::imp::ImpersonateOS::Linux => r#""Linux""#, - crate::imp::ImpersonateOS::Android => r#""Android""#, - crate::imp::ImpersonateOS::IOS => r#""iOS""#, - _ => os_platform(crate::imp::random_impersonate_os()), +/// Builds HTTP/2 settings for an Edge version. +#[cfg(feature = "http2")] +fn build_http2_settings(edge: Impersonate) -> crate::imp::Http2Data { + // Edge 146+ uses different header order (sec-ch-ua after sec-fetch-*) + let headers_order = if matches!( + edge, + Impersonate::EdgeV146 | Impersonate::EdgeV147 | Impersonate::EdgeV148 + ) { + Some(crate::imp::header_order_upgrade_first_sec_chua_last().clone()) + } else { + Some(crate::imp::header_order_sec_chua_first().clone()) + }; + + crate::imp::Http2Data { + settings_order: Some(edge_settings_order().clone()), + headers_pseudo_order: Some(edge_pseudo_order().clone()), + headers_order, + headers_priority: Some((255, 0, true)), + initial_stream_window_size: Some(crate::imp::CHROME_INITIAL_STREAM_WINDOW), + initial_connection_window_size: Some(crate::imp::CHROME_INITIAL_CONNECTION_WINDOW), + max_header_list_size: Some(crate::imp::CHROME_MAX_HEADER_LIST_SIZE), + header_table_size: Some(crate::imp::CHROME_HEADER_TABLE_SIZE), + ..Default::default() + } +} + +fn edge_emulator(edge: Impersonate) -> Arc { + match edge { + Impersonate::EdgeV144 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_edge_emulator(144))).clone() + } + Impersonate::EdgeV145 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_edge_emulator(145))).clone() + } + Impersonate::EdgeV146 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_edge_emulator(146))).clone() + } + Impersonate::EdgeV147 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_edge_emulator(147))).clone() + } + Impersonate::EdgeV148 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_edge_emulator(148))).clone() + } + _ => unreachable!(), } } -/// Builds HTTP/2 settings for an Edge version. -#[cfg(feature = "http2")] -fn build_http2_settings(_edge: Impersonate) -> crate::imp::Http2Data { - use crate::imp::{PseudoId, PseudoOrder, SettingId, SettingsOrder}; +fn new_edge_emulator(major: u16) -> BrowserEmulator { + let mut emulator = BrowserEmulator::new(BrowserType::Edge, BrowserVersion::new(major, 0, 0)); + emulator.cipher_suites = Some(emulation::cipher_suites::EDGE.to_vec()); + emulator.signature_algorithms = Some(emulation::signature_algorithms::EDGE.to_vec()); + emulator.named_groups = Some(emulation::named_groups::EDGE.to_vec()); + emulator.extension_order_seed = Some(emulation::extension_order::EDGE); + emulator +} + +fn base_edge_headers() -> &'static http::HeaderMap { + static BASE: OnceLock = OnceLock::new(); + BASE.get_or_init(|| { + let mut headers = http::HeaderMap::with_capacity(13); + headers.insert(ACCEPT, http::HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")); + headers.insert("accept-encoding", http::HeaderValue::from_static("gzip, deflate, br, zstd")); + headers.insert("accept-language", http::HeaderValue::from_static("en-US,en;q=0.9")); + headers.insert("upgrade-insecure-requests", http::HeaderValue::from_static("1")); + headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); + headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); + headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); + headers.insert("sec-fetch-user", http::HeaderValue::from_static("?1")); + headers.insert("priority", http::HeaderValue::from_static("u=0, i")); + headers + }) +} - // Edge 144 sends only settings: 1, 2, 4, 6 (same as Chrome) - // Expected fingerprint: 1:65536;2:0;4:6291456;6:262144 - let settings_order = Some( +#[cfg(feature = "http2")] +fn edge_settings_order() -> &'static SettingsOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { SettingsOrder::builder() - .push(SettingId::HeaderTableSize) // 1: 65536 - .push(SettingId::EnablePush) // 2: 0 - .push(SettingId::InitialWindowSize) // 4: 6291456 - .push(SettingId::MaxHeaderListSize) // 6: 262144 - .build_without_extend(), - ); + .push(SettingId::HeaderTableSize) + .push(SettingId::EnablePush) + .push(SettingId::InitialWindowSize) + .push(SettingId::MaxHeaderListSize) + .build_without_extend() + }) +} - let headers_pseudo_order = Some( +#[cfg(feature = "http2")] +fn edge_pseudo_order() -> &'static PseudoOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { PseudoOrder::builder() .push(PseudoId::Method) .push(PseudoId::Authority) .push(PseudoId::Scheme) .push(PseudoId::Path) - .build(), - ); - - crate::imp::Http2Data { - initial_stream_window_size: Some(6291456), - initial_connection_window_size: Some(15728640), - max_concurrent_streams: None, - max_frame_size: None, - max_header_list_size: Some(262144), - header_table_size: Some(65536), - enable_push: Some(false), - enable_connect_protocol: None, - no_rfc7540_priorities: None, - settings_order, - headers_pseudo_order, - headers_priority: Some((255, 0, true)), - headers_order: Some(vec![ - http::HeaderName::from_static("sec-ch-ua"), - http::HeaderName::from_static("sec-ch-ua-mobile"), - http::HeaderName::from_static("sec-ch-ua-platform"), - http::HeaderName::from_static("upgrade-insecure-requests"), - http::HeaderName::from_static("user-agent"), - http::HeaderName::from_static("accept"), - http::HeaderName::from_static("sec-fetch-site"), - http::HeaderName::from_static("sec-fetch-mode"), - http::HeaderName::from_static("sec-fetch-user"), - http::HeaderName::from_static("sec-fetch-dest"), - http::HeaderName::from_static("accept-encoding"), - http::HeaderName::from_static("accept-language"), - http::HeaderName::from_static("priority"), - ]), - initial_stream_id: None, - } + .build() + }) } #[cfg(test)] diff --git a/crates/primp/src/imp/firefox/mod.rs b/crates/primp/src/imp/firefox/mod.rs index 749aaf9..7557a9c 100644 --- a/crates/primp/src/imp/firefox/mod.rs +++ b/crates/primp/src/imp/firefox/mod.rs @@ -1,109 +1,45 @@ //! Firefox browser impersonation settings. -//! -//! This module provides configuration for impersonating various Firefox browser versions. -//! Each version has its own TLS fingerprint, ALPN protocols, and default HTTP headers -//! that mimic the real Firefox browser behavior. -//! -//! # Usage -//! -//! ```rust -//! use primp::{Client, Impersonate}; -//! -//! #[tokio::main] -//! async fn main() -> Result<(), Box> { -//! let client = Client::builder() -//! .impersonate(Impersonate::FirefoxV140) -//! .build()?; -//! -//! //let response = client.get("https://example.com").send().await?; -//! Ok(()) -//! } -//! ``` pub use crate::imp::Impersonate; +use rustls::client::{BrowserEmulator, BrowserType, BrowserVersion}; +use rustls::crypto::emulation; +use std::sync::{Arc, OnceLock}; /// Builds browser settings for a specific Firefox version and OS. pub(crate) fn build_firefox_settings( firefox: Impersonate, os: crate::imp::ImpersonateOS, ) -> crate::imp::BrowserSettings { - use rustls::client::{BrowserEmulator, BrowserType, BrowserVersion}; - use rustls::crypto::emulation; - use std::sync::OnceLock; - let user_agent = build_user_agent(firefox, os); let headers = build_headers(user_agent); - // Get cached browser emulator for Firefox (avoids Vec allocations on each call) let browser_emulator = match firefox { Impersonate::FirefoxV140 => { - static FIREFOX_140: OnceLock = OnceLock::new(); - FIREFOX_140 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Firefox, BrowserVersion::new(140, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::FIREFOX.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::FIREFOX.to_vec()); - emulator.named_groups = Some(emulation::named_groups::FIREFOX.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::FIREFOX); - emulator - }) + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_firefox_emulator(140))) .clone() } Impersonate::FirefoxV146 => { - static FIREFOX_146: OnceLock = OnceLock::new(); - FIREFOX_146 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Firefox, BrowserVersion::new(146, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::FIREFOX.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::FIREFOX.to_vec()); - emulator.named_groups = Some(emulation::named_groups::FIREFOX.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::FIREFOX); - emulator - }) + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_firefox_emulator(146))) .clone() } Impersonate::FirefoxV147 => { - static FIREFOX_147: OnceLock = OnceLock::new(); - FIREFOX_147 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Firefox, BrowserVersion::new(147, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::FIREFOX.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::FIREFOX.to_vec()); - emulator.named_groups = Some(emulation::named_groups::FIREFOX.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::FIREFOX); - emulator - }) + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_firefox_emulator(147))) .clone() } Impersonate::FirefoxV148 => { - static FIREFOX_148: OnceLock = OnceLock::new(); - FIREFOX_148 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Firefox, BrowserVersion::new(148, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::FIREFOX.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::FIREFOX.to_vec()); - emulator.named_groups = Some(emulation::named_groups::FIREFOX.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::FIREFOX); - emulator - }) + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_firefox_emulator(148))) .clone() } _ => unreachable!(), }; - let http2 = build_http2_settings(); - crate::imp::BrowserSettings { browser_emulator, - http2, + http2: build_http2_settings(), headers, gzip: true, brotli: true, @@ -112,6 +48,15 @@ pub(crate) fn build_firefox_settings( } } +fn new_firefox_emulator(major: u16) -> BrowserEmulator { + let mut emulator = BrowserEmulator::new(BrowserType::Firefox, BrowserVersion::new(major, 0, 0)); + emulator.cipher_suites = Some(emulation::cipher_suites::FIREFOX.to_vec()); + emulator.signature_algorithms = Some(emulation::signature_algorithms::FIREFOX.to_vec()); + emulator.named_groups = Some(emulation::named_groups::FIREFOX.to_vec()); + emulator.extension_order_seed = Some(emulation::extension_order::FIREFOX); + emulator +} + /// Builds a User-Agent string for a Firefox version and OS. fn build_user_agent(firefox: Impersonate, os: crate::imp::ImpersonateOS) -> &'static str { match firefox { @@ -151,89 +96,100 @@ fn build_user_agent(firefox: Impersonate, os: crate::imp::ImpersonateOS) -> &'st } } +fn firefox_base_headers() -> &'static http::HeaderMap { + static BASE: OnceLock = OnceLock::new(); + BASE.get_or_init(|| { + let mut headers = http::HeaderMap::with_capacity(12); + headers.insert( + http::header::ACCEPT, + http::HeaderValue::from_static( + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + ), + ); + headers.insert( + "accept-language", + http::HeaderValue::from_static("en-US,en;q=0.5"), + ); + headers.insert( + "accept-encoding", + http::HeaderValue::from_static("gzip, deflate, br, zstd"), + ); + headers.insert("dnt", http::HeaderValue::from_static("1")); + headers.insert("sec-gpc", http::HeaderValue::from_static("1")); + headers.insert( + "upgrade-insecure-requests", + http::HeaderValue::from_static("1"), + ); + headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); + headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); + headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); + headers.insert("sec-fetch-user", http::HeaderValue::from_static("?1")); + headers.insert("priority", http::HeaderValue::from_static("u=0, i")); + headers.insert("te", http::HeaderValue::from_static("trailers")); + headers + }) +} + /// Builds default headers for Firefox. fn build_headers(user_agent: &'static str) -> http::HeaderMap { - use http::header::*; - - let mut headers = http::HeaderMap::with_capacity(13); - headers.insert(USER_AGENT, http::HeaderValue::from_static(user_agent)); - headers.insert( - ACCEPT, - http::HeaderValue::from_static( - "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - ), - ); - headers.insert( - "accept-language", - http::HeaderValue::from_static("en-US,en;q=0.5"), - ); - headers.insert( - "accept-encoding", - http::HeaderValue::from_static("gzip, deflate, br, zstd"), - ); - headers.insert("dnt", http::HeaderValue::from_static("1")); - headers.insert("sec-gpc", http::HeaderValue::from_static("1")); + let mut headers = firefox_base_headers().clone(); headers.insert( - "upgrade-insecure-requests", - http::HeaderValue::from_static("1"), + http::header::USER_AGENT, + http::HeaderValue::from_static(user_agent), ); - headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); - headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); - headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); - headers.insert("sec-fetch-user", http::HeaderValue::from_static("?1")); - headers.insert("priority", http::HeaderValue::from_static("u=0, i")); - headers.insert("te", http::HeaderValue::from_static("trailers")); - headers } /// Builds HTTP/2 settings for Firefox. -/// -/// Firefox 140/146 HTTP/2 fingerprint: -/// - Settings: 1:65536, 2:0, 4:131072, 5:16384 -/// - Window Update: 12517377 -/// - Pseudo-header order: m,p,a,s (:method, :path, :authority, :scheme) #[cfg(feature = "http2")] fn build_http2_settings() -> crate::imp::Http2Data { - use crate::imp::{PseudoId, PseudoOrder, SettingId, SettingsOrder}; - - // Firefox sends settings: 1, 2, 4, 5 - // Expected fingerprint: 1:65536;2:0;4:131072;5:16384 - let settings_order = Some( - SettingsOrder::builder() - .push(SettingId::HeaderTableSize) // 1: 65536 - .push(SettingId::EnablePush) // 2: 0 - .push(SettingId::InitialWindowSize) // 4: 131072 - .push(SettingId::MaxFrameSize) // 5: 16384 - .build_without_extend(), - ); - - // Firefox pseudo-header order: m,p,a,s (:method, :path, :authority, :scheme) - let headers_pseudo_order = Some( - PseudoOrder::builder() - .push(PseudoId::Method) - .push(PseudoId::Path) - .push(PseudoId::Authority) - .push(PseudoId::Scheme) - .build(), - ); - crate::imp::Http2Data { - initial_stream_window_size: Some(131072), - // Note: h2 subtracts the default window size (65535) from this value, - // so we add 65535 to get the target window update of 12517377 - initial_connection_window_size: Some(12517377 + 65535), - max_concurrent_streams: None, + initial_stream_window_size: Some(crate::imp::FIREFOX_INITIAL_STREAM_WINDOW), + initial_connection_window_size: Some(crate::imp::FIREFOX_INITIAL_CONNECTION_WINDOW), max_frame_size: Some(16384), - max_header_list_size: None, - header_table_size: Some(65536), + header_table_size: Some(crate::imp::FIREFOX_HEADER_TABLE_SIZE), enable_push: Some(false), - enable_connect_protocol: None, - no_rfc7540_priorities: None, - settings_order, - headers_pseudo_order, - headers_priority: Some((41, 0, false)), // Firefox: weight=41 (displays as 42 on wire after +1), dep=0, excl=0 - headers_order: Some(vec![ + settings_order: Some(firefox_settings_order().clone()), + headers_pseudo_order: Some(firefox_pseudo_order().clone()), + headers_priority: Some((41, 0, false)), + headers_order: Some(firefox_headers_order().clone()), + initial_stream_id: Some(3), + initial_stream_window_size_increment: Some(12451840), + ..Default::default() + } +} + +#[cfg(feature = "http2")] +fn firefox_settings_order() -> &'static crate::imp::SettingsOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { + crate::imp::SettingsOrder::builder() + .push(crate::imp::SettingId::HeaderTableSize) + .push(crate::imp::SettingId::EnablePush) + .push(crate::imp::SettingId::InitialWindowSize) + .push(crate::imp::SettingId::MaxFrameSize) + .build_without_extend() + }) +} + +#[cfg(feature = "http2")] +fn firefox_pseudo_order() -> &'static crate::imp::PseudoOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { + crate::imp::PseudoOrder::builder() + .push(crate::imp::PseudoId::Method) + .push(crate::imp::PseudoId::Path) + .push(crate::imp::PseudoId::Authority) + .push(crate::imp::PseudoId::Scheme) + .build() + }) +} + +#[cfg(feature = "http2")] +fn firefox_headers_order() -> &'static Vec { + static ORDER: OnceLock> = OnceLock::new(); + ORDER.get_or_init(|| { + vec![ http::HeaderName::from_static("user-agent"), http::HeaderName::from_static("accept"), http::HeaderName::from_static("accept-language"), @@ -247,9 +203,8 @@ fn build_http2_settings() -> crate::imp::Http2Data { http::HeaderName::from_static("sec-fetch-user"), http::HeaderName::from_static("priority"), http::HeaderName::from_static("te"), - ]), - initial_stream_id: Some(3), // Firefox starts at stream ID 3 - } + ] + }) } #[cfg(test)] diff --git a/crates/primp/src/imp/mod.rs b/crates/primp/src/imp/mod.rs index 01a19c0..6772cb0 100644 --- a/crates/primp/src/imp/mod.rs +++ b/crates/primp/src/imp/mod.rs @@ -27,6 +27,7 @@ use http::HeaderMap; use rand::prelude::*; use rustls::client::BrowserEmulator; +use std::sync::{Arc, OnceLock}; // Re-export h2 types for HTTP/2 fingerprinting when http2 feature is enabled #[cfg(feature = "http2")] @@ -40,6 +41,20 @@ pub mod firefox; pub mod opera; pub mod safari; +// HTTP/2 magic numbers grouped by browser family. +pub(crate) const CHROME_INITIAL_STREAM_WINDOW: u32 = 6291456; +pub(crate) const CHROME_INITIAL_CONNECTION_WINDOW: u32 = 15728640; +pub(crate) const CHROME_MAX_HEADER_LIST_SIZE: u32 = 262144; +pub(crate) const CHROME_HEADER_TABLE_SIZE: u32 = 65536; + +pub(crate) const FIREFOX_INITIAL_STREAM_WINDOW: u32 = 131072; +pub(crate) const FIREFOX_INITIAL_CONNECTION_WINDOW: u32 = 12517377 + 65535; // 12582912 +pub(crate) const FIREFOX_HEADER_TABLE_SIZE: u32 = 65536; + +pub(crate) const SAFARI_INITIAL_STREAM_WINDOW: u32 = 2097152; +pub(crate) const SAFARI_INITIAL_CONNECTION_WINDOW: u32 = 10485760; +pub(crate) const SAFARI_MAX_HEADER_LIST_SIZE: u32 = 262144; + /// Browser TLS and HTTP/2 configuration settings. /// /// This struct contains all the configuration needed to impersonate a specific @@ -48,7 +63,7 @@ pub mod safari; #[derive(Clone)] pub struct BrowserSettings { /// Rustls browser emulator configuration - pub(crate) browser_emulator: BrowserEmulator, + pub(crate) browser_emulator: Arc, /// HTTP/2 configuration data pub http2: Http2Data, /// Default headers to include with requests @@ -145,6 +160,14 @@ pub struct Http2Data { /// Default: 1 (standard HTTP/2 behavior) /// Firefox uses 3 (skips stream 1) pub initial_stream_id: Option, + + /// Extra receive window capacity to add to new locally-initiated streams. + /// + /// When set, after creating a new stream, a WINDOW_UPDATE frame will be sent + /// to increase the stream's receive window by this amount. + /// This is used for browser fingerprinting (e.g. Firefox adds 12451840 + /// to the first stream's receive window). + pub initial_stream_window_size_increment: Option, } impl Default for Http2Data { @@ -164,6 +187,7 @@ impl Default for Http2Data { headers_priority: None, headers_order: None, initial_stream_id: None, + initial_stream_window_size_increment: None, } } } @@ -185,12 +209,16 @@ pub enum Impersonate { ChromeV144, ChromeV145, ChromeV146, + ChromeV147, + ChromeV148, /// Random Chrome version Chrome, // Edge variants EdgeV144, EdgeV145, EdgeV146, + EdgeV147, + EdgeV148, /// Random Edge version Edge, // Opera variants @@ -198,6 +226,8 @@ pub enum Impersonate { OperaV127, OperaV128, OperaV129, + OperaV130, + OperaV131, /// Random Opera version Opera, // Safari variants @@ -252,13 +282,19 @@ pub fn random_impersonate() -> Impersonate { Impersonate::ChromeV144, Impersonate::ChromeV145, Impersonate::ChromeV146, + Impersonate::ChromeV147, + Impersonate::ChromeV148, Impersonate::EdgeV144, Impersonate::EdgeV145, Impersonate::EdgeV146, + Impersonate::EdgeV147, + Impersonate::EdgeV148, Impersonate::OperaV126, Impersonate::OperaV127, Impersonate::OperaV128, Impersonate::OperaV129, + Impersonate::OperaV130, + Impersonate::OperaV131, Impersonate::SafariV26, Impersonate::SafariV26_3, Impersonate::SafariV18_5, @@ -280,6 +316,8 @@ pub fn resolve_impersonate(version: Impersonate) -> Impersonate { Impersonate::ChromeV144, Impersonate::ChromeV145, Impersonate::ChromeV146, + Impersonate::ChromeV147, + Impersonate::ChromeV148, ]; *CHROME.choose(&mut rand::rng()).unwrap() } @@ -288,6 +326,8 @@ pub fn resolve_impersonate(version: Impersonate) -> Impersonate { Impersonate::EdgeV144, Impersonate::EdgeV145, Impersonate::EdgeV146, + Impersonate::EdgeV147, + Impersonate::EdgeV148, ]; *EDGE.choose(&mut rand::rng()).unwrap() } @@ -297,6 +337,8 @@ pub fn resolve_impersonate(version: Impersonate) -> Impersonate { Impersonate::OperaV127, Impersonate::OperaV128, Impersonate::OperaV129, + Impersonate::OperaV130, + Impersonate::OperaV131, ]; *OPERA.choose(&mut rand::rng()).unwrap() } @@ -339,6 +381,85 @@ pub fn random_impersonate_os() -> ImpersonateOS { *OS_VARIANTS.choose(&mut rand::rng()).unwrap() } +/// Returns the OS-specific sec-ch-ua-platform header value. +pub(crate) fn os_platform(os: ImpersonateOS) -> &'static str { + match os { + ImpersonateOS::Windows => r#""Windows""#, + ImpersonateOS::MacOS => r#""macOS""#, + ImpersonateOS::Linux => r#""Linux""#, + ImpersonateOS::Android => r#""Android""#, + ImpersonateOS::IOS => r#""iOS""#, + ImpersonateOS::Random => os_platform(random_impersonate_os()), + } +} + +/// Standard Chrome/Edge/Opera header order with sec-ch-ua first. +pub(crate) fn header_order_sec_chua_first() -> &'static Vec { + static ORDER: OnceLock> = OnceLock::new(); + ORDER.get_or_init(|| { + vec![ + http::HeaderName::from_static("sec-ch-ua"), + http::HeaderName::from_static("sec-ch-ua-mobile"), + http::HeaderName::from_static("sec-ch-ua-platform"), + http::HeaderName::from_static("upgrade-insecure-requests"), + http::HeaderName::from_static("user-agent"), + http::HeaderName::from_static("accept"), + http::HeaderName::from_static("sec-fetch-site"), + http::HeaderName::from_static("sec-fetch-mode"), + http::HeaderName::from_static("sec-fetch-user"), + http::HeaderName::from_static("sec-fetch-dest"), + http::HeaderName::from_static("accept-encoding"), + http::HeaderName::from_static("accept-language"), + http::HeaderName::from_static("priority"), + ] + }) +} + +/// Chrome 148+ / Edge 146+ header order with sec-ch-ua after sec-fetch-*. +pub(crate) fn header_order_upgrade_first_sec_chua_last() -> &'static Vec { + static ORDER: OnceLock> = OnceLock::new(); + ORDER.get_or_init(|| { + vec![ + http::HeaderName::from_static("upgrade-insecure-requests"), + http::HeaderName::from_static("user-agent"), + http::HeaderName::from_static("accept"), + http::HeaderName::from_static("sec-fetch-site"), + http::HeaderName::from_static("sec-fetch-mode"), + http::HeaderName::from_static("sec-fetch-user"), + http::HeaderName::from_static("sec-fetch-dest"), + http::HeaderName::from_static("sec-ch-ua"), + http::HeaderName::from_static("sec-ch-ua-mobile"), + http::HeaderName::from_static("sec-ch-ua-platform"), + http::HeaderName::from_static("accept-encoding"), + http::HeaderName::from_static("accept-language"), + http::HeaderName::from_static("priority"), + ] + }) +} + +/// Opera 131+ header order with cache-control first. +pub(crate) fn header_order_cache_control_first() -> &'static Vec { + static ORDER: OnceLock> = OnceLock::new(); + ORDER.get_or_init(|| { + vec![ + http::HeaderName::from_static("cache-control"), + http::HeaderName::from_static("sec-ch-ua"), + http::HeaderName::from_static("sec-ch-ua-mobile"), + http::HeaderName::from_static("sec-ch-ua-platform"), + http::HeaderName::from_static("upgrade-insecure-requests"), + http::HeaderName::from_static("user-agent"), + http::HeaderName::from_static("accept"), + http::HeaderName::from_static("sec-fetch-site"), + http::HeaderName::from_static("sec-fetch-mode"), + http::HeaderName::from_static("sec-fetch-user"), + http::HeaderName::from_static("sec-fetch-dest"), + http::HeaderName::from_static("accept-encoding"), + http::HeaderName::from_static("accept-language"), + http::HeaderName::from_static("priority"), + ] + }) +} + /// Gets browser settings for impersonation. /// /// # Arguments @@ -358,16 +479,22 @@ pub fn get_browser_settings( let os_type = os_type.unwrap_or_default(); match version { - Impersonate::ChromeV144 | Impersonate::ChromeV145 | Impersonate::ChromeV146 => { - chrome::build_chrome_settings(version, os_type) - } - Impersonate::EdgeV144 | Impersonate::EdgeV145 | Impersonate::EdgeV146 => { - edge::build_edge_settings(version, os_type) - } + Impersonate::ChromeV144 + | Impersonate::ChromeV145 + | Impersonate::ChromeV146 + | Impersonate::ChromeV147 + | Impersonate::ChromeV148 => chrome::build_chrome_settings(version, os_type), + Impersonate::EdgeV144 + | Impersonate::EdgeV145 + | Impersonate::EdgeV146 + | Impersonate::EdgeV147 + | Impersonate::EdgeV148 => edge::build_edge_settings(version, os_type), Impersonate::OperaV126 | Impersonate::OperaV127 | Impersonate::OperaV128 - | Impersonate::OperaV129 => opera::build_opera_settings(version, os_type), + | Impersonate::OperaV129 + | Impersonate::OperaV130 + | Impersonate::OperaV131 => opera::build_opera_settings(version, os_type), Impersonate::SafariV26 | Impersonate::SafariV26_3 | Impersonate::SafariV18_5 => { safari::build_safari_settings(version, os_type) } diff --git a/crates/primp/src/imp/opera/mod.rs b/crates/primp/src/imp/opera/mod.rs index 3bb9fb6..833da1e 100644 --- a/crates/primp/src/imp/opera/mod.rs +++ b/crates/primp/src/imp/opera/mod.rs @@ -22,26 +22,23 @@ //! ``` pub use crate::imp::Impersonate; +#[cfg(feature = "http2")] +use crate::imp::{PseudoId, PseudoOrder, SettingId, SettingsOrder}; +use http::header::*; +use rustls::client::{BrowserEmulator, BrowserType, BrowserVersion}; +use rustls::crypto::emulation; +use std::sync::{Arc, OnceLock}; /// Builds browser settings for a specific Opera version and OS. pub(crate) fn build_opera_settings( opera: Impersonate, os: crate::imp::ImpersonateOS, ) -> crate::imp::BrowserSettings { - use http::header::*; - use rustls::client::{BrowserEmulator, BrowserType, BrowserVersion}; - use rustls::crypto::emulation; - use std::sync::OnceLock; - let user_agent = build_user_agent(opera, os); let sec_ch_ua = build_sec_ch_ua(opera, os); - let mut headers = http::HeaderMap::with_capacity(13); + let mut headers = base_opera_headers().clone(); headers.insert(USER_AGENT, http::HeaderValue::from_static(user_agent)); - headers.insert( - "upgrade-insecure-requests", - http::HeaderValue::from_static("1"), - ); headers.insert("sec-ch-ua", http::HeaderValue::from_static(sec_ch_ua)); headers.insert( "sec-ch-ua-mobile", @@ -58,88 +55,16 @@ pub(crate) fn build_opera_settings( ); headers.insert( "sec-ch-ua-platform", - http::HeaderValue::from_static(os_platform(os)), + http::HeaderValue::from_static(crate::imp::os_platform(os)), ); - headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); - headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); - headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); - headers.insert("sec-fetch-user", http::HeaderValue::from_static("?1")); - headers.insert(ACCEPT, http::HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")); - headers.insert( - "accept-encoding", - http::HeaderValue::from_static("gzip, deflate, br, zstd"), - ); - headers.insert( - "accept-language", - http::HeaderValue::from_static("en-US,en;q=0.9"), - ); - headers.insert("priority", http::HeaderValue::from_static("u=0, i")); + + // Opera 131+ adds cache-control as the first header + if matches!(opera, Impersonate::OperaV131) { + headers.insert("cache-control", http::HeaderValue::from_static("max-age=0")); + } // Get cached browser emulator for Opera (avoids Vec allocations on each call) - // Opera is Chrome-based, so use Chrome fingerprints for all versions - let browser_emulator = match opera { - Impersonate::OperaV126 => { - static OPERA_126: OnceLock = OnceLock::new(); - OPERA_126 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Opera, BrowserVersion::new(126, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::CHROME.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::CHROME.to_vec()); - emulator.named_groups = Some(emulation::named_groups::CHROME.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::CHROME); - emulator - }) - .clone() - } - Impersonate::OperaV127 => { - static OPERA_127: OnceLock = OnceLock::new(); - OPERA_127 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Opera, BrowserVersion::new(127, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::CHROME.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::CHROME.to_vec()); - emulator.named_groups = Some(emulation::named_groups::CHROME.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::CHROME); - emulator - }) - .clone() - } - Impersonate::OperaV128 => { - static OPERA_128: OnceLock = OnceLock::new(); - OPERA_128 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Opera, BrowserVersion::new(128, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::CHROME.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::CHROME.to_vec()); - emulator.named_groups = Some(emulation::named_groups::CHROME.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::CHROME); - emulator - }) - .clone() - } - Impersonate::OperaV129 => { - static OPERA_129: OnceLock = OnceLock::new(); - OPERA_129 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Opera, BrowserVersion::new(129, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::CHROME.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::CHROME.to_vec()); - emulator.named_groups = Some(emulation::named_groups::CHROME.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::CHROME); - emulator - }) - .clone() - } - _ => unreachable!(), - }; + let browser_emulator = opera_emulator(opera); let http2 = build_http2_settings(opera); @@ -193,6 +118,24 @@ fn build_user_agent(opera: Impersonate, os: crate::imp::ImpersonateOS) -> &'stat crate::imp::ImpersonateOS::IOS => "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) OPiOS/129.0.0.0 Mobile/15E148 Safari/605.1.15", _ => build_user_agent(opera, crate::imp::random_impersonate_os()), }, + // Opera 130 is based on Chrome 146 + Impersonate::OperaV130 => match os { + crate::imp::ImpersonateOS::Windows => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 OPR/130.0.0.0", + crate::imp::ImpersonateOS::MacOS => "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 OPR/130.0.0.0", + crate::imp::ImpersonateOS::Linux => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 OPR/130.0.0.0", + crate::imp::ImpersonateOS::Android => "Mozilla/5.0 (Linux; Android 10; SM-G970F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36 OPR/130.0.0.0", + crate::imp::ImpersonateOS::IOS => "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) OPiOS/130.0.0.0 Mobile/15E148 Safari/605.1.15", + _ => build_user_agent(opera, crate::imp::random_impersonate_os()), + }, + // Opera 131 is based on Chrome 147 + Impersonate::OperaV131 => match os { + crate::imp::ImpersonateOS::Windows => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 OPR/131.0.0.0", + crate::imp::ImpersonateOS::MacOS => "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 OPR/131.0.0.0", + crate::imp::ImpersonateOS::Linux => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 OPR/131.0.0.0", + crate::imp::ImpersonateOS::Android => "Mozilla/5.0 (Linux; Android 10; SM-G970F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36 OPR/131.0.0.0", + crate::imp::ImpersonateOS::IOS => "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) OPiOS/131.0.0.0 Mobile/15E148 Safari/605.1.15", + _ => build_user_agent(opera, crate::imp::random_impersonate_os()), + }, _ => unreachable!(), } } @@ -200,79 +143,125 @@ fn build_user_agent(opera: Impersonate, os: crate::imp::ImpersonateOS) -> &'stat /// Builds a sec-ch-ua header value for an Opera version and OS. fn build_sec_ch_ua(opera: Impersonate, _os: crate::imp::ImpersonateOS) -> &'static str { match opera { - Impersonate::OperaV126 => r#""Not:A-Brand";v="99", "Opera";v="126", "Chromium";v="142""#, - Impersonate::OperaV127 => r#""Not:A-Brand";v="99", "Opera";v="127", "Chromium";v="143""#, - Impersonate::OperaV128 => r#""Not:A-Brand";v="99", "Opera";v="128", "Chromium";v="144""#, + Impersonate::OperaV126 => r#""Chromium";v="142", "Opera";v="126", "Not_A Brand";v="99""#, + Impersonate::OperaV127 => r#""Opera";v="127", "Chromium";v="143", "Not A(Brand";v="24""#, + Impersonate::OperaV128 => r#""Not(A:Brand";v="8", "Chromium";v="144", "Opera";v="128""#, Impersonate::OperaV129 => r#""Not:A-Brand";v="99", "Opera";v="129", "Chromium";v="145""#, + Impersonate::OperaV130 => r#""Chromium";v="146", "Not-A.Brand";v="24", "Opera";v="130""#, + Impersonate::OperaV131 => r#""Opera";v="131", "Not.A/Brand";v="8", "Chromium";v="147""#, _ => unreachable!(), } } -/// Returns a platform string for sec-ch-ua-platform header. -fn os_platform(os: crate::imp::ImpersonateOS) -> &'static str { - match os { - crate::imp::ImpersonateOS::Windows => r#""Windows""#, - crate::imp::ImpersonateOS::MacOS => r#""macOS""#, - crate::imp::ImpersonateOS::Linux => r#""Linux""#, - crate::imp::ImpersonateOS::Android => r#""Android""#, - crate::imp::ImpersonateOS::IOS => r#""iOS""#, - _ => os_platform(crate::imp::random_impersonate_os()), +/// Builds HTTP/2 settings for an Opera version. +#[cfg(feature = "http2")] +fn build_http2_settings(opera: Impersonate) -> crate::imp::Http2Data { + // Opera 131 uses cache-control-first header order + let headers_order = if matches!(opera, Impersonate::OperaV131) { + Some(crate::imp::header_order_cache_control_first().clone()) + } else { + Some(crate::imp::header_order_sec_chua_first().clone()) + }; + + crate::imp::Http2Data { + settings_order: Some(opera_settings_order().clone()), + headers_pseudo_order: Some(opera_pseudo_order().clone()), + headers_order, + headers_priority: Some((255, 0, true)), + initial_stream_window_size: Some(crate::imp::CHROME_INITIAL_STREAM_WINDOW), + initial_connection_window_size: Some(crate::imp::CHROME_INITIAL_CONNECTION_WINDOW), + max_header_list_size: Some(crate::imp::CHROME_MAX_HEADER_LIST_SIZE), + header_table_size: Some(crate::imp::CHROME_HEADER_TABLE_SIZE), + ..Default::default() } } -/// Builds HTTP/2 settings for an Opera version. -#[cfg(feature = "http2")] -fn build_http2_settings(_opera: Impersonate) -> crate::imp::Http2Data { - use crate::imp::{PseudoId, PseudoOrder, SettingId, SettingsOrder}; +fn opera_emulator(opera: Impersonate) -> Arc { + match opera { + Impersonate::OperaV126 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_opera_emulator(126))) + .clone() + } + Impersonate::OperaV127 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_opera_emulator(127))) + .clone() + } + Impersonate::OperaV128 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_opera_emulator(128))) + .clone() + } + Impersonate::OperaV129 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_opera_emulator(129))) + .clone() + } + Impersonate::OperaV130 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_opera_emulator(130))) + .clone() + } + Impersonate::OperaV131 => { + static EMU: OnceLock> = OnceLock::new(); + EMU.get_or_init(|| Arc::new(new_opera_emulator(131))) + .clone() + } + _ => unreachable!(), + } +} + +fn new_opera_emulator(major: u16) -> BrowserEmulator { + let mut emulator = BrowserEmulator::new(BrowserType::Opera, BrowserVersion::new(major, 0, 0)); + emulator.cipher_suites = Some(emulation::cipher_suites::CHROME.to_vec()); + emulator.signature_algorithms = Some(emulation::signature_algorithms::CHROME.to_vec()); + emulator.named_groups = Some(emulation::named_groups::CHROME.to_vec()); + emulator.extension_order_seed = Some(emulation::extension_order::CHROME); + emulator +} - let settings_order = Some( +fn base_opera_headers() -> &'static http::HeaderMap { + static BASE: OnceLock = OnceLock::new(); + BASE.get_or_init(|| { + let mut headers = http::HeaderMap::with_capacity(13); + headers.insert(ACCEPT, http::HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")); + headers.insert("accept-encoding", http::HeaderValue::from_static("gzip, deflate, br, zstd")); + headers.insert("accept-language", http::HeaderValue::from_static("en-US,en;q=0.9")); + headers.insert("upgrade-insecure-requests", http::HeaderValue::from_static("1")); + headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); + headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); + headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); + headers.insert("sec-fetch-user", http::HeaderValue::from_static("?1")); + headers.insert("priority", http::HeaderValue::from_static("u=0, i")); + headers + }) +} + +#[cfg(feature = "http2")] +fn opera_settings_order() -> &'static SettingsOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { SettingsOrder::builder() - .push(SettingId::HeaderTableSize) // 1: 65536 - .push(SettingId::EnablePush) // 2: 0 - .push(SettingId::InitialWindowSize) // 4: 6291456 - .push(SettingId::MaxHeaderListSize) // 6: 262144 - .build_without_extend(), - ); + .push(SettingId::HeaderTableSize) + .push(SettingId::EnablePush) + .push(SettingId::InitialWindowSize) + .push(SettingId::MaxHeaderListSize) + .build_without_extend() + }) +} - let headers_pseudo_order = Some( +#[cfg(feature = "http2")] +fn opera_pseudo_order() -> &'static PseudoOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { PseudoOrder::builder() .push(PseudoId::Method) .push(PseudoId::Authority) .push(PseudoId::Scheme) .push(PseudoId::Path) - .build(), - ); - - crate::imp::Http2Data { - initial_stream_window_size: Some(6291456), - initial_connection_window_size: Some(15728640), - max_concurrent_streams: None, - max_frame_size: None, - max_header_list_size: Some(262144), - header_table_size: Some(65536), - enable_push: Some(false), - enable_connect_protocol: None, - no_rfc7540_priorities: None, - settings_order, - headers_pseudo_order, - headers_priority: Some((255, 0, true)), - headers_order: Some(vec![ - http::HeaderName::from_static("sec-ch-ua"), - http::HeaderName::from_static("sec-ch-ua-mobile"), - http::HeaderName::from_static("sec-ch-ua-platform"), - http::HeaderName::from_static("upgrade-insecure-requests"), - http::HeaderName::from_static("user-agent"), - http::HeaderName::from_static("accept"), - http::HeaderName::from_static("sec-fetch-site"), - http::HeaderName::from_static("sec-fetch-mode"), - http::HeaderName::from_static("sec-fetch-user"), - http::HeaderName::from_static("sec-fetch-dest"), - http::HeaderName::from_static("accept-encoding"), - http::HeaderName::from_static("accept-language"), - http::HeaderName::from_static("priority"), - ]), - initial_stream_id: None, - } + .build() + }) } #[cfg(test)] diff --git a/crates/primp/src/imp/safari/mod.rs b/crates/primp/src/imp/safari/mod.rs index 3429d78..ac72c77 100644 --- a/crates/primp/src/imp/safari/mod.rs +++ b/crates/primp/src/imp/safari/mod.rs @@ -21,18 +21,19 @@ //! ``` pub use crate::imp::Impersonate; +#[cfg(feature = "http2")] +use crate::imp::{PseudoId, PseudoOrder, SettingId, SettingsOrder}; +use rustls::client::{BrowserEmulator, BrowserEmulatorOS, BrowserType, BrowserVersion}; +use rustls::crypto::emulation; +use std::sync::{Arc, OnceLock}; /// Builds browser settings for a specific Safari version and OS. pub(crate) fn build_safari_settings( safari: Impersonate, os: crate::imp::ImpersonateOS, ) -> crate::imp::BrowserSettings { - use rustls::client::{BrowserEmulator, BrowserEmulatorOS, BrowserType, BrowserVersion}; - use rustls::crypto::emulation; - use std::sync::OnceLock; - let user_agent = build_user_agent(safari, os); - let headers = build_headers(user_agent); + let headers = build_safari_base_headers(user_agent); // Convert ImpersonateOS to BrowserEmulatorOS let browser_os = match os { @@ -42,86 +43,15 @@ pub(crate) fn build_safari_settings( }; // Get cached browser emulator for Safari (avoids Vec allocations on each call) - // Note: We cache the base emulator and set OS type on the clone - let mut browser_emulator = match safari { - Impersonate::SafariV18_5 => { - static SAFARI_18_5: OnceLock = OnceLock::new(); - SAFARI_18_5 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Safari, BrowserVersion::new(18, 5, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::SAFARI.to_vec()); - emulator.named_groups = Some(emulation::named_groups::SAFARI.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::SAFARI.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::SAFARI_18_5); - emulator.include_status_request_v2 = true; - emulator - }) - .clone() - } - Impersonate::SafariV26 => { - static SAFARI_26: OnceLock = OnceLock::new(); - SAFARI_26 - .get_or_init(|| { - let mut emulator = - BrowserEmulator::new(BrowserType::Safari, BrowserVersion::new(26, 0, 0)); - emulator.cipher_suites = Some(emulation::cipher_suites::SAFARI.to_vec()); - emulator.named_groups = Some(emulation::named_groups::SAFARI.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::SAFARI.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::SAFARI_26); - emulator - }) - .clone() - } - Impersonate::SafariV26_3 => { - if browser_os == BrowserEmulatorOS::IOS { - static SAFARI_26_3_IOS: OnceLock = OnceLock::new(); - SAFARI_26_3_IOS - .get_or_init(|| { - let mut emulator = BrowserEmulator::new( - BrowserType::Safari, - BrowserVersion::new(26, 3, 0), - ); - emulator.cipher_suites = Some(emulation::cipher_suites::SAFARI.to_vec()); - emulator.named_groups = Some(emulation::named_groups::SAFARI.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::SAFARI.to_vec()); - emulator.extension_order_seed = Some(emulation::extension_order::SAFARI_26); - emulator - }) - .clone() - } else { - static SAFARI_26_3_MACOS: OnceLock = OnceLock::new(); - SAFARI_26_3_MACOS - .get_or_init(|| { - let mut emulator = BrowserEmulator::new( - BrowserType::Safari, - BrowserVersion::new(26, 3, 0), - ); - emulator.cipher_suites = Some(emulation::cipher_suites::SAFARI.to_vec()); - emulator.named_groups = Some(emulation::named_groups::SAFARI.to_vec()); - emulator.signature_algorithms = - Some(emulation::signature_algorithms::SAFARI.to_vec()); - emulator.extension_order_seed = - Some(emulation::extension_order::SAFARI_18_5); - emulator.include_status_request_v2 = true; - emulator - }) - .clone() - } - } - _ => unreachable!(), - }; + let mut browser_emulator = safari_emulator(safari, browser_os); // Set OS type on the cloned emulator browser_emulator.os_type = Some(browser_os); - let http2 = build_http2_settings(os); + let http2 = build_http2_settings(); crate::imp::BrowserSettings { - browser_emulator, + browser_emulator: Arc::new(browser_emulator), http2, headers, gzip: true, @@ -154,80 +84,144 @@ fn build_user_agent(safari: Impersonate, os: crate::imp::ImpersonateOS) -> &'sta } } -/// Builds default headers for Safari. -/// -/// Safari does NOT support Client Hints, so we don't include sec-ch-ua headers. -fn build_headers(user_agent: &'static str) -> http::HeaderMap { - use http::header::*; - - let mut headers = http::HeaderMap::with_capacity(8); - headers.insert(USER_AGENT, http::HeaderValue::from_static(user_agent)); +/// Builds default headers for Safari using cached base. +fn build_safari_base_headers(user_agent: &'static str) -> http::HeaderMap { + let mut headers = safari_base_headers().clone(); headers.insert( - ACCEPT, - http::HeaderValue::from_static( - "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - ), + http::header::USER_AGENT, + http::HeaderValue::from_static(user_agent), ); - headers.insert( - ACCEPT_LANGUAGE, - http::HeaderValue::from_static("en-US,en;q=0.9"), - ); - headers.insert( - "accept-encoding", - http::HeaderValue::from_static("gzip, deflate, br"), - ); - headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); - headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); - headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); - headers.insert("priority", http::HeaderValue::from_static("u=0, i")); - headers } /// Builds HTTP/2 settings for Safari. #[cfg(feature = "http2")] -fn build_http2_settings(_os: crate::imp::ImpersonateOS) -> crate::imp::Http2Data { - use crate::imp::{PseudoId, PseudoOrder, SettingId, SettingsOrder}; +fn build_http2_settings() -> crate::imp::Http2Data { + crate::imp::Http2Data { + initial_stream_window_size: Some(crate::imp::SAFARI_INITIAL_STREAM_WINDOW), + initial_connection_window_size: Some(crate::imp::SAFARI_INITIAL_CONNECTION_WINDOW), + max_concurrent_streams: Some(100), + max_header_list_size: Some(crate::imp::SAFARI_MAX_HEADER_LIST_SIZE), + no_rfc7540_priorities: Some(true), + settings_order: Some(safari_settings_order().clone()), + headers_pseudo_order: Some(safari_pseudo_order().clone()), + ..Default::default() + } +} + +fn safari_emulator(safari: Impersonate, browser_os: BrowserEmulatorOS) -> BrowserEmulator { + match safari { + Impersonate::SafariV18_5 => { + static EMU: OnceLock = OnceLock::new(); + EMU.get_or_init(new_safari_18_5_emulator).clone() + } + Impersonate::SafariV26 => { + static EMU: OnceLock = OnceLock::new(); + EMU.get_or_init(new_safari_26_emulator).clone() + } + Impersonate::SafariV26_3 => { + if browser_os == BrowserEmulatorOS::IOS { + static EMU_IOS: OnceLock = OnceLock::new(); + EMU_IOS.get_or_init(new_safari_26_3_ios_emulator).clone() + } else { + static EMU_MACOS: OnceLock = OnceLock::new(); + EMU_MACOS + .get_or_init(new_safari_26_3_macos_emulator) + .clone() + } + } + _ => unreachable!(), + } +} + +fn new_safari_18_5_emulator() -> BrowserEmulator { + let mut emulator = BrowserEmulator::new(BrowserType::Safari, BrowserVersion::new(18, 5, 0)); + emulator.cipher_suites = Some(emulation::cipher_suites::SAFARI.to_vec()); + emulator.named_groups = Some(emulation::named_groups::SAFARI.to_vec()); + emulator.signature_algorithms = Some(emulation::signature_algorithms::SAFARI.to_vec()); + emulator.extension_order_seed = Some(emulation::extension_order::SAFARI_18_5); + emulator.include_status_request_v2 = true; + emulator +} + +fn new_safari_26_emulator() -> BrowserEmulator { + let mut emulator = BrowserEmulator::new(BrowserType::Safari, BrowserVersion::new(26, 0, 0)); + emulator.cipher_suites = Some(emulation::cipher_suites::SAFARI.to_vec()); + emulator.named_groups = Some(emulation::named_groups::SAFARI.to_vec()); + emulator.signature_algorithms = Some(emulation::signature_algorithms::SAFARI.to_vec()); + emulator.extension_order_seed = Some(emulation::extension_order::SAFARI_26); + emulator +} + +fn new_safari_26_3_ios_emulator() -> BrowserEmulator { + let mut emulator = BrowserEmulator::new(BrowserType::Safari, BrowserVersion::new(26, 3, 0)); + emulator.cipher_suites = Some(emulation::cipher_suites::SAFARI.to_vec()); + emulator.named_groups = Some(emulation::named_groups::SAFARI.to_vec()); + emulator.signature_algorithms = Some(emulation::signature_algorithms::SAFARI.to_vec()); + emulator.extension_order_seed = Some(emulation::extension_order::SAFARI_26); + emulator +} - // Safari 26 HTTP/2 settings (version-specific, same for macOS and iOS) - // Settings order: 2, 3, 4, 9 - let settings_order = Some( +fn new_safari_26_3_macos_emulator() -> BrowserEmulator { + let mut emulator = BrowserEmulator::new(BrowserType::Safari, BrowserVersion::new(26, 3, 0)); + emulator.cipher_suites = Some(emulation::cipher_suites::SAFARI.to_vec()); + emulator.named_groups = Some(emulation::named_groups::SAFARI.to_vec()); + emulator.signature_algorithms = Some(emulation::signature_algorithms::SAFARI.to_vec()); + emulator.extension_order_seed = Some(emulation::extension_order::SAFARI_18_5); + emulator.include_status_request_v2 = true; + emulator +} + +fn safari_base_headers() -> &'static http::HeaderMap { + static BASE: OnceLock = OnceLock::new(); + BASE.get_or_init(|| { + let mut headers = http::HeaderMap::with_capacity(8); + headers.insert( + http::header::ACCEPT, + http::HeaderValue::from_static( + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + ), + ); + headers.insert( + http::header::ACCEPT_LANGUAGE, + http::HeaderValue::from_static("en-US,en;q=0.9"), + ); + headers.insert( + "accept-encoding", + http::HeaderValue::from_static("gzip, deflate, br"), + ); + headers.insert("sec-fetch-dest", http::HeaderValue::from_static("document")); + headers.insert("sec-fetch-mode", http::HeaderValue::from_static("navigate")); + headers.insert("sec-fetch-site", http::HeaderValue::from_static("none")); + headers.insert("priority", http::HeaderValue::from_static("u=0, i")); + headers + }) +} + +#[cfg(feature = "http2")] +fn safari_settings_order() -> &'static SettingsOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { SettingsOrder::builder() .push(SettingId::EnablePush) .push(SettingId::MaxConcurrentStreams) .push(SettingId::InitialWindowSize) .push(SettingId::NoRfc7540Priorities) - .build_without_extend(), - ); + .build_without_extend() + }) +} - // Safari 26 pseudo order: Method, Scheme, Authority, Path - let headers_pseudo_order = Some( +#[cfg(feature = "http2")] +fn safari_pseudo_order() -> &'static PseudoOrder { + static ORDER: OnceLock = OnceLock::new(); + ORDER.get_or_init(|| { PseudoOrder::builder() .push(PseudoId::Method) .push(PseudoId::Scheme) .push(PseudoId::Authority) .push(PseudoId::Path) - .build(), - ); - - crate::imp::Http2Data { - // Safari sends: 2:0;3:100;4:2097152;9:1 - // WINDOW_UPDATE delta = 10420225, so initial_connection_window_size = 10420225 + 65535 = 10485760 - initial_stream_window_size: Some(2097152), - initial_connection_window_size: Some(10485760), - max_concurrent_streams: Some(100), - max_frame_size: None, - max_header_list_size: None, - header_table_size: None, - enable_push: Some(false), - enable_connect_protocol: None, - no_rfc7540_priorities: Some(true), - settings_order, - headers_pseudo_order, - headers_priority: None, - headers_order: None, - initial_stream_id: None, - } + .build() + }) } #[cfg(test)] diff --git a/crates/primp/src/lib.rs b/crates/primp/src/lib.rs index 68400a1..5762876 100644 --- a/crates/primp/src/lib.rs +++ b/crates/primp/src/lib.rs @@ -618,7 +618,7 @@ fn build_impersonate_tls_config( }; // Set browser emulation - config.browser_emulation = Some(settings.browser_emulator.clone()); + config.browser_emulation = Some((*settings.browser_emulator).clone()); // Set ALPN protocols config.alpn_protocols = vec!["h2".into(), "http/1.1".into()]; @@ -693,6 +693,9 @@ fn apply_http2_settings( if let Some(stream_id) = http2.initial_stream_id { builder = builder.http2_initial_stream_id(stream_id); } + if let Some(incr) = http2.initial_stream_window_size_increment { + builder = builder.http2_initial_stream_window_size_increment(incr); + } builder }