diff --git a/Cargo.lock b/Cargo.lock index e06ac75e7..942d5f64b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,15 +65,15 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arraydeque" @@ -83,9 +83,9 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -112,7 +112,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -123,7 +123,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -146,9 +146,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" @@ -158,20 +158,20 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] [[package]] name = "bitstream-io" -version = "4.9.0" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" dependencies = [ - "core2", + "no_std_io2", ] [[package]] @@ -221,9 +221,9 @@ checksum = "d8e6738dfb11354886f890621b4a34c0b177f75538023f7100b608ab9adbd66b" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" @@ -239,9 +239,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.48" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", @@ -330,18 +330,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.59" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.59" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", @@ -349,15 +349,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "brotli", "compression-core", @@ -367,18 +367,18 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "config" -version = "0.15.19" +version = "0.15.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" dependencies = [ "async-trait", - "convert_case", + "convert_case 0.6.0", "json5", "pathdiff", "ron", @@ -386,8 +386,8 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml 0.9.8", - "winnow 0.7.14", + "toml", + "winnow", "yaml-rust2", ] @@ -412,7 +412,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] @@ -426,6 +426,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -442,15 +451,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -552,7 +552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -579,7 +579,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -603,7 +603,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -614,7 +614,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -629,9 +629,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -639,22 +639,24 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case 0.10.0", "proc-macro2", "quote", - "syn 2.0.111", + "rustc_version", + "syn 2.0.117", "unicode-xid", ] @@ -687,7 +689,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -707,9 +709,9 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -786,8 +788,8 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "thiserror 2.0.17", - "toml 1.0.7+spec-1.1.0", + "thiserror 2.0.18", + "toml", "tower-service", "tracing", "validator", @@ -803,8 +805,8 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.111", - "toml 1.0.7+spec-1.1.0", + "syn 2.0.117", + "toml", "validator", ] @@ -851,12 +853,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - [[package]] name = "equivalent" version = "1.0.2" @@ -865,25 +861,15 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", "typeid", ] -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - [[package]] name = "error-stack" version = "0.6.0" @@ -896,9 +882,9 @@ dependencies = [ [[package]] name = "fastly" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71bbe202b3ec57a5af4beeeb8012d1fe7b058bc1532077fa3625abacfa9cdc95" +checksum = "5f767502306f09f6dcb76302d09cd2ea8542e228d5f155166f0c2da925e16c61" dependencies = [ "anyhow", "bytes", @@ -924,9 +910,9 @@ dependencies = [ [[package]] name = "fastly-macros" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4595b318e91b27b4924fec250fd456479808618071854d23a4748a05e711dd0" +checksum = "51ae08eeeb5ed0c1a8b454fc89dca0e316e13b7889e81fc9a435503c1e84a2d7" dependencies = [ "proc-macro2", "quote", @@ -935,9 +921,9 @@ dependencies = [ [[package]] name = "fastly-shared" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7588a6d13dfab9e3c8ea5602fea17436fc2bcad14de980c18a50a441a49fe4b2" +checksum = "d64ed1bba12ca45d1a2a80c2c55d903297adb3eeb4edc9d327c1d51ee709d404" dependencies = [ "bitflags 1.3.2", "http", @@ -945,21 +931,21 @@ dependencies = [ [[package]] name = "fastly-sys" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "932e5d862257fa58bde67264532d91da2603396331a82bfb7b75a89b3a670de8" +checksum = "8f1b82ebd99583740a074d8962ca75d7d17065b185a94e4919c3a3f2193268b6" dependencies = [ "bitflags 1.3.2", "fastly-shared", "wasip2", - "wit-bindgen", + "wit-bindgen 0.46.0", ] [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fern" @@ -988,15 +974,15 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1085,7 +1071,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1130,9 +1116,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1141,14 +1127,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", + "wasip3", ] [[package]] @@ -1199,6 +1186,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hashlink" version = "0.10.0" @@ -1271,8 +1264,8 @@ dependencies = [ "proc-macro2", "quote", "strum_macros", - "syn 2.0.111", - "thiserror 2.0.17", + "syn 2.0.117", + "thiserror 2.0.18", "walkdir", ] @@ -1284,14 +1277,14 @@ checksum = "d5acda598b043c6386d20fffe86c600b63c7ca4980ee9a28f7e9aaa15d749747" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1313,12 +1306,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1326,9 +1320,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1339,9 +1333,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1353,15 +1347,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1373,15 +1367,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1392,6 +1386,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1421,12 +1421,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -1469,9 +1471,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jose-b64" @@ -1511,9 +1513,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -1540,28 +1542,28 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.182" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] -name = "libm" -version = "0.2.15" +name = "libc" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "linux-raw-sys" -version = "0.11.0" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1580,9 +1582,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "log-fastly" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc98cdb87b71bb8808549dae92b5f4915bfaed44339f1134b62f3bf145c0ad7" +checksum = "58a5d864949b863161476a8129ef0322c56c77bb15f98d88991002072f497b1e" dependencies = [ "fastly", "log", @@ -1595,7 +1597,7 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ff94cb6aef6ee52afd2c69331e9109906d855e82bd241f3110dfdf6185899ab" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cssparser", "encoding_rs", @@ -1605,20 +1607,20 @@ dependencies = [ "mime", "precomputed-hash", "selectors", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "matchit" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3eede3bdf92f3b4f9dc04072a9ce5ab557d5ec9038773bf9ffcd5588b3cc05b" +checksum = "8863b587001c1b9a8a4e36008cebc6b3612cb1226fe2de94858e06092687b608" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -1642,6 +1644,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "no_std_io2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +dependencies = [ + "memchr", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1660,9 +1671,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-derive" @@ -1672,7 +1683,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1707,9 +1718,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -1790,9 +1801,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -1800,9 +1811,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -1810,22 +1821,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2 0.10.9", @@ -1872,7 +1883,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1886,9 +1897,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkcs1" @@ -1924,9 +1935,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1959,7 +1970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1990,38 +2001,38 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", @@ -2044,7 +2055,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -2053,7 +2064,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] @@ -2070,9 +2081,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2081,17 +2092,17 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ron" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "once_cell", "serde", "serde_derive", @@ -2131,9 +2142,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -2144,19 +2155,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -2165,9 +2163,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -2203,7 +2201,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cssparser", "derive_more", "log", @@ -2218,9 +2216,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2261,7 +2259,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2285,14 +2283,14 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -2360,21 +2358,21 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2419,7 +2417,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2441,9 +2439,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2458,7 +2456,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2481,11 +2479,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -2496,18 +2494,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2552,9 +2550,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -2572,9 +2570,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "pin-project-lite", @@ -2583,75 +2581,53 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "toml" -version = "0.9.8" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" -dependencies = [ - "serde_core", - "serde_spanned", - "toml_datetime 0.7.3", - "toml_parser", - "winnow 0.7.14", -] - -[[package]] -name = "toml" -version = "1.0.7+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 1.0.1+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.0", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" -dependencies = [ - "serde_core", + "winnow", ] [[package]] name = "toml_datetime" -version = "1.0.1+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow", ] [[package]] name = "toml_writer" -version = "1.0.7+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower-service" @@ -2678,7 +2654,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2739,6 +2715,7 @@ dependencies = [ "log", "lol_html", "matchit", + "mime", "rand", "regex", "serde", @@ -2747,7 +2724,7 @@ dependencies = [ "subtle", "temp-env", "tokio", - "toml 1.0.7+spec-1.1.0", + "toml", "trusted-server-js", "trusted-server-openrtb", "url", @@ -2783,9 +2760,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -2795,15 +2772,15 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -2847,11 +2824,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -2883,7 +2860,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2910,18 +2887,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "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.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -2932,9 +2918,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2942,26 +2928,60 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] +[[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 = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2974,13 +2994,11 @@ dependencies = [ [[package]] name = "which" -version = "8.0.0" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "env_home", - "rustix", - "winsafe", + "libc", ] [[package]] @@ -3013,7 +3031,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3024,7 +3042,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3062,39 +3080,124 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] [[package]] -name = "winnow" -version = "1.0.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags 2.11.1", +] [[package]] -name = "winsafe" -version = "0.0.19" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", +] + +[[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 2.0.117", + "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 2.0.117", + "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 2.11.1", + "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.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yaml-rust2" @@ -3109,9 +3212,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3120,54 +3223,54 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "synstructure", ] @@ -3182,9 +3285,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3193,9 +3296,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3204,13 +3307,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0b9f42309..9f2f4c673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ log = "0.4.29" log-fastly = "0.11.12" lol_html = "2.7.2" matchit = "0.9" +mime = "0.3" rand = "0.8" regex = "1.12.3" serde = { version = "1.0", features = ["derive"] } diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index 52c869d7f..a14338ba9 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -5,13 +5,13 @@ use fastly::{Request, Response}; use trusted_server_core::auction::endpoints::handle_auction; use trusted_server_core::auction::{build_orchestrator, AuctionOrchestrator}; use trusted_server_core::auth::enforce_basic_auth; +use trusted_server_core::compat; use trusted_server_core::constants::{ ENV_FASTLY_IS_STAGING, ENV_FASTLY_SERVICE_VERSION, HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_TS_ENV, HEADER_X_TS_VERSION, }; use trusted_server_core::error::TrustedServerError; use trusted_server_core::geo::GeoInfo; -use trusted_server_core::http_util::sanitize_forwarded_headers; use trusted_server_core::integrations::IntegrationRegistry; use trusted_server_core::platform::RuntimeServices; use trusted_server_core::proxy::{ @@ -119,7 +119,7 @@ async fn route_request( // Strip client-spoofable forwarded headers at the edge. // On Fastly this service IS the first proxy — these headers from // clients are untrusted and can hijack URL rewriting (see #409). - sanitize_forwarded_headers(&mut req); + compat::sanitize_fastly_forwarded_headers(&mut req); // Look up geo info via the platform abstraction using the client IP // already captured in RuntimeServices at the entry point. @@ -134,8 +134,10 @@ async fn route_request( // `get_settings()` should already have rejected invalid handler regexes. // Keep this fallback so manually-constructed or otherwise unprepared // settings still become an error response instead of panicking. - match enforce_basic_auth(settings, &req) { - Ok(Some(mut response)) => { + let auth_req = compat::from_fastly_headers_ref(&req); + match enforce_basic_auth(settings, &auth_req) { + Ok(Some(response)) => { + let mut response = compat::to_fastly_response(response); finalize_response(settings, geo_info.as_ref(), &mut response); return Some(response); } diff --git a/crates/trusted-server-core/Cargo.toml b/crates/trusted-server-core/Cargo.toml index 9c69bd30b..95ef3a035 100644 --- a/crates/trusted-server-core/Cargo.toml +++ b/crates/trusted-server-core/Cargo.toml @@ -31,6 +31,7 @@ http = { workspace = true } iab_gpp = { workspace = true } jose-jwk = { workspace = true } log = { workspace = true } +mime = { workspace = true } rand = { workspace = true } lol_html = { workspace = true } matchit = { workspace = true } diff --git a/crates/trusted-server-core/src/auction/endpoints.rs b/crates/trusted-server-core/src/auction/endpoints.rs index cafa36f18..df51a8fe9 100644 --- a/crates/trusted-server-core/src/auction/endpoints.rs +++ b/crates/trusted-server-core/src/auction/endpoints.rs @@ -4,6 +4,7 @@ use error_stack::{Report, ResultExt}; use fastly::{Request, Response}; use crate::auction::formats::AdRequest; +use crate::compat; use crate::consent; use crate::cookies::handle_request_cookies; use crate::edge_cookie::get_or_generate_ec_id; @@ -46,6 +47,8 @@ pub async fn handle_auction( body.ad_units.len() ); + let http_req = compat::from_fastly_headers_ref(&req); + // Generate EC ID early so the consent pipeline can use it for // KV Store fallback/write operations. let ec_id = get_or_generate_ec_id(settings, services, &req).change_context( @@ -55,7 +58,7 @@ pub async fn handle_auction( )?; // Extract consent from request cookies, headers, and geo. - let cookie_jar = handle_request_cookies(&req)?; + let cookie_jar = handle_request_cookies(&http_req)?; let geo = services .geo() .lookup(services.client_info.client_ip) @@ -65,7 +68,7 @@ pub async fn handle_auction( }); let consent_context = consent::build_consent_context(&consent::ConsentPipelineInput { jar: cookie_jar.as_ref(), - req: &req, + req: &http_req, config: &settings.consent, geo: geo.as_ref(), ec_id: Some(ec_id.as_str()), diff --git a/crates/trusted-server-core/src/auth.rs b/crates/trusted-server-core/src/auth.rs index 547784dfe..fa8820440 100644 --- a/crates/trusted-server-core/src/auth.rs +++ b/crates/trusted-server-core/src/auth.rs @@ -1,7 +1,8 @@ use base64::{engine::general_purpose::STANDARD, Engine as _}; +use edgezero_core::body::Body as EdgeBody; use error_stack::Report; -use fastly::http::{header, StatusCode}; -use fastly::{Request, Response}; +use http::header; +use http::{Request, Response, StatusCode}; use sha2::{Digest as _, Sha256}; use subtle::ConstantTimeEq as _; @@ -27,9 +28,9 @@ const BASIC_AUTH_REALM: &str = r#"Basic realm="Trusted Server""#; /// un-compilable path regex. pub fn enforce_basic_auth( settings: &Settings, - req: &Request, -) -> Result, Report> { - let Some(handler) = settings.handler_for_path(req.get_path())? else { + req: &Request, +) -> Result>, Report> { + let Some(handler) = settings.handler_for_path(req.uri().path())? else { return Ok(None); }; @@ -53,14 +54,15 @@ pub fn enforce_basic_auth( if bool::from(username_match & password_match) { Ok(None) } else { - log::warn!("Basic auth failed for path: {}", req.get_path()); + log::warn!("Basic auth failed for path: {}", req.uri().path()); Ok(Some(unauthorized_response())) } } -fn extract_credentials(req: &Request) -> Option<(String, String)> { +fn extract_credentials(req: &Request) -> Option<(String, String)> { let header_value = req - .get_header(header::AUTHORIZATION) + .headers() + .get(header::AUTHORIZATION) .and_then(|value| value.to_str().ok())?; let mut parts = header_value.splitn(2, ' '); @@ -84,25 +86,42 @@ fn extract_credentials(req: &Request) -> Option<(String, String)> { Some((username, password)) } -fn unauthorized_response() -> Response { - Response::from_status(StatusCode::UNAUTHORIZED) - .with_header(header::WWW_AUTHENTICATE, BASIC_AUTH_REALM) - .with_header(header::CONTENT_TYPE, "text/plain; charset=utf-8") - .with_body_text_plain("Unauthorized") +fn unauthorized_response() -> Response { + Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(header::WWW_AUTHENTICATE, BASIC_AUTH_REALM) + .header(header::CONTENT_TYPE, "text/plain; charset=utf-8") + .body(EdgeBody::from(b"Unauthorized".as_ref())) + .expect("should build unauthorized response") } #[cfg(test)] mod tests { use super::*; use base64::engine::general_purpose::STANDARD; - use fastly::http::{header, Method}; + use http::{header, HeaderValue, Method}; use crate::test_support::tests::{crate_test_settings_str, create_test_settings}; + fn build_request(method: Method, uri: &str) -> Request { + Request::builder() + .method(method) + .uri(uri) + .body(EdgeBody::empty()) + .expect("should build request") + } + + fn set_authorization(req: &mut Request, value: &str) { + req.headers_mut().insert( + header::AUTHORIZATION, + HeaderValue::from_str(value).expect("should build authorization header"), + ); + } + #[test] fn no_challenge_for_non_protected_path() { let settings = create_test_settings(); - let req = Request::new(Method::GET, "https://example.com/open"); + let req = build_request(Method::GET, "https://example.com/open"); assert!(enforce_basic_auth(&settings, &req) .expect("should evaluate auth") @@ -112,14 +131,15 @@ mod tests { #[test] fn challenge_when_missing_credentials() { let settings = create_test_settings(); - let req = Request::new(Method::GET, "https://example.com/secure"); + let req = build_request(Method::GET, "https://example.com/secure"); let response = enforce_basic_auth(&settings, &req) .expect("should evaluate auth") .expect("should challenge"); - assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); let realm = response - .get_header(header::WWW_AUTHENTICATE) + .headers() + .get(header::WWW_AUTHENTICATE) .expect("should have WWW-Authenticate header"); assert_eq!(realm, BASIC_AUTH_REALM); } @@ -127,9 +147,9 @@ mod tests { #[test] fn allow_when_credentials_match() { let settings = create_test_settings(); - let mut req = Request::new(Method::GET, "https://example.com/secure/data"); + let mut req = build_request(Method::GET, "https://example.com/secure/data"); let token = STANDARD.encode("user:pass"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + set_authorization(&mut req, &format!("Basic {token}")); assert!(enforce_basic_auth(&settings, &req) .expect("should evaluate auth") @@ -139,29 +159,29 @@ mod tests { #[test] fn challenge_when_both_credentials_wrong() { let settings = create_test_settings(); - let mut req = Request::new(Method::GET, "https://example.com/secure/data"); + let mut req = build_request(Method::GET, "https://example.com/secure/data"); let token = STANDARD.encode("wrong:wrong"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + set_authorization(&mut req, &format!("Basic {token}")); let response = enforce_basic_auth(&settings, &req) .expect("should evaluate auth") .expect("should challenge"); - assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[test] fn challenge_when_username_wrong_password_correct() { // Validates that both fields are always evaluated — no short-circuit username oracle. let settings = create_test_settings(); - let mut req = Request::new(Method::GET, "https://example.com/secure/data"); + let mut req = build_request(Method::GET, "https://example.com/secure/data"); let token = STANDARD.encode("wrong-user:pass"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + set_authorization(&mut req, &format!("Basic {token}")); let response = enforce_basic_auth(&settings, &req) .expect("should evaluate auth") .expect("should challenge"); assert_eq!( - response.get_status(), + response.status(), StatusCode::UNAUTHORIZED, "should reject wrong username even with correct password" ); @@ -170,15 +190,15 @@ mod tests { #[test] fn challenge_when_username_correct_password_wrong() { let settings = create_test_settings(); - let mut req = Request::new(Method::GET, "https://example.com/secure/data"); + let mut req = build_request(Method::GET, "https://example.com/secure/data"); let token = STANDARD.encode("user:wrong-pass"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + set_authorization(&mut req, &format!("Basic {token}")); let response = enforce_basic_auth(&settings, &req) .expect("should evaluate auth") .expect("should challenge"); assert_eq!( - response.get_status(), + response.status(), StatusCode::UNAUTHORIZED, "should reject correct username with wrong password" ); @@ -187,13 +207,13 @@ mod tests { #[test] fn challenge_when_scheme_is_not_basic() { let settings = create_test_settings(); - let mut req = Request::new(Method::GET, "https://example.com/secure"); - req.set_header(header::AUTHORIZATION, "Bearer token"); + let mut req = build_request(Method::GET, "https://example.com/secure"); + set_authorization(&mut req, "Bearer token"); let response = enforce_basic_auth(&settings, &req) .expect("should evaluate auth") .expect("should challenge"); - assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[test] @@ -210,9 +230,9 @@ mod tests { #[test] fn allow_admin_path_with_valid_credentials() { let settings = create_test_settings(); - let mut req = Request::new(Method::POST, "https://example.com/admin/keys/rotate"); + let mut req = build_request(Method::POST, "https://example.com/admin/keys/rotate"); let token = STANDARD.encode("admin:admin-pass"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + set_authorization(&mut req, &format!("Basic {token}")); assert!( enforce_basic_auth(&settings, &req) @@ -225,24 +245,24 @@ mod tests { #[test] fn challenge_admin_path_with_wrong_credentials() { let settings = create_test_settings(); - let mut req = Request::new(Method::POST, "https://example.com/admin/keys/rotate"); + let mut req = build_request(Method::POST, "https://example.com/admin/keys/rotate"); let token = STANDARD.encode("admin:wrong"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + set_authorization(&mut req, &format!("Basic {token}")); let response = enforce_basic_auth(&settings, &req) .expect("should evaluate auth") .expect("should challenge admin path with wrong credentials"); - assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[test] fn challenge_admin_path_with_missing_credentials() { let settings = create_test_settings(); - let req = Request::new(Method::POST, "https://example.com/admin/keys/rotate"); + let req = build_request(Method::POST, "https://example.com/admin/keys/rotate"); let response = enforce_basic_auth(&settings, &req) .expect("should evaluate auth") .expect("should challenge admin path with missing credentials"); - assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } } diff --git a/crates/trusted-server-core/src/compat.rs b/crates/trusted-server-core/src/compat.rs new file mode 100644 index 000000000..4c6d53728 --- /dev/null +++ b/crates/trusted-server-core/src/compat.rs @@ -0,0 +1,394 @@ +//! Compatibility bridge between `fastly` SDK types and `http` crate types. +//! +//! All items in this module are temporary scaffolding created in PR 11 and +//! scheduled for deletion in PR 15. Do not add new callers after PR 13. +//! +//! # PR 15 removal target + +use edgezero_core::body::Body as EdgeBody; +use fastly::http::header; + +use crate::constants::INTERNAL_HEADERS; +use crate::http_util::SPOOFABLE_FORWARDED_HEADERS; + +fn build_http_request(req: &fastly::Request, body: EdgeBody) -> http::Request { + let uri: http::Uri = req.get_url_str().parse().unwrap_or_else(|_| { + log::warn!( + "Failed to parse request URL '{}'; falling back to '/'", + req.get_url_str() + ); + http::Uri::from_static("/") + }); + + let mut builder = http::Request::builder() + .method(req.get_method().clone()) + .uri(uri); + + for (name, value) in req.get_headers() { + builder = builder.header(name.as_str(), value.as_bytes()); + } + + // Cannot fail: URI is always valid (parsed above or the "/" fallback), + // and Fastly pre-validates all method and header values. + builder + .body(body) + .expect("should build http request from fastly request") +} + +/// Convert a borrowed `fastly::Request` into an `http::Request` for reading. +/// +/// Headers are copied; the body is empty. +/// +/// # PR 15 removal target +/// +/// # Panics +/// +/// Does not panic in practice — URL parse failure falls back to `"/"` (logged +/// as a warning), and the subsequent `builder.body()` cannot fail given a valid +/// method and URI. Listed here only because clippy cannot prove it statically. +pub fn from_fastly_headers_ref(req: &fastly::Request) -> http::Request { + build_http_request(req, EdgeBody::empty()) +} + +/// Convert an `http::Response` into a `fastly::Response`. +/// +/// # PR 15 removal target +pub fn to_fastly_response(resp: http::Response) -> fastly::Response { + let (parts, body) = resp.into_parts(); + let mut fastly_resp = fastly::Response::from_status(parts.status.as_u16()); + for (name, value) in &parts.headers { + fastly_resp.append_header(name.as_str(), value.as_bytes()); + } + + debug_assert!( + matches!(&body, EdgeBody::Once(_)), + "streaming body passed to compat::to_fastly_response will be silently truncated" + ); + match body { + EdgeBody::Once(bytes) => { + if !bytes.is_empty() { + fastly_resp.set_body(bytes.to_vec()); + } + } + EdgeBody::Stream(_) => { + log::warn!("streaming body in compat::to_fastly_response; body will be empty"); + } + } + + fastly_resp +} + +/// Sanitize forwarded headers on a `fastly::Request`. +/// +/// # PR 15 removal target +pub fn sanitize_fastly_forwarded_headers(req: &mut fastly::Request) { + for &name in SPOOFABLE_FORWARDED_HEADERS { + if req.get_header(name).is_some() { + log::debug!("Stripped spoofable header: {name}"); + req.remove_header(name); + } + } +} + +/// Copy `X-*` custom headers between two `fastly::Request` values. +/// +/// # PR 15 removal target +pub fn copy_fastly_custom_headers(from: &fastly::Request, to: &mut fastly::Request) { + for (name, value) in from.get_headers() { + let name_str = name.as_str(); + if name_str.starts_with("x-") && !INTERNAL_HEADERS.contains(&name_str) { + to.append_header(name_str, value); + } + } +} + +/// Forward the `Cookie` header from one `fastly::Request` to another. +/// +/// # PR 15 removal target +pub fn forward_fastly_cookie_header( + from: &fastly::Request, + to: &mut fastly::Request, + strip_consent: bool, +) { + use crate::cookies::{strip_cookies, CONSENT_COOKIE_NAMES}; + + let Some(cookie_value) = from.get_header(header::COOKIE) else { + return; + }; + + if !strip_consent { + to.set_header(header::COOKIE, cookie_value); + return; + } + + match cookie_value.to_str() { + Ok(value) => { + let stripped = strip_cookies(value, CONSENT_COOKIE_NAMES); + if !stripped.is_empty() { + to.set_header(header::COOKIE, &stripped); + } + } + Err(_) => { + to.set_header(header::COOKIE, cookie_value); + } + } +} + +/// Set the EC ID cookie on a `fastly::Response`. +/// +/// # PR 15 removal target +pub fn set_fastly_ec_cookie( + settings: &crate::settings::Settings, + response: &mut fastly::Response, + ec_id: &str, +) { + if !crate::cookies::ec_cookie_value_is_safe(ec_id) { + log::warn!( + "Rejecting EC ID for Set-Cookie: value of {} bytes contains characters illegal in a cookie value", + ec_id.len() + ); + return; + } + response.append_header( + header::SET_COOKIE, + crate::cookies::create_ec_cookie(settings, ec_id), + ); +} + +/// Expire the EC ID cookie on a `fastly::Response`. +/// +/// # PR 15 removal target +pub fn expire_fastly_ec_cookie( + settings: &crate::settings::Settings, + response: &mut fastly::Response, +) { + response.append_header( + header::SET_COOKIE, + format!( + "{}=; {}", + crate::constants::COOKIE_TS_EC, + crate::cookies::ec_cookie_attributes(settings, 0), + ), + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_once_body_eq(body: EdgeBody, expected: &[u8]) { + match body { + EdgeBody::Once(bytes) => assert_eq!(bytes.as_ref(), expected, "should copy body bytes"), + EdgeBody::Stream(_) => panic!("expected non-streaming body"), + } + } + + #[test] + fn from_fastly_headers_ref_copies_headers() { + let mut fastly_req = + fastly::Request::new(fastly::http::Method::GET, "https://example.com/path"); + fastly_req.set_header("x-custom", "value"); + + let http_req = from_fastly_headers_ref(&fastly_req); + + assert_eq!(http_req.uri().path(), "/path", "should copy path"); + assert_eq!( + http_req + .headers() + .get("x-custom") + .and_then(|v| v.to_str().ok()), + Some("value"), + "should copy custom header" + ); + } + + #[test] + fn from_fastly_headers_ref_preserves_duplicate_headers() { + let mut fastly_req = + fastly::Request::new(fastly::http::Method::GET, "https://example.com/path"); + fastly_req.append_header("x-custom", "first"); + fastly_req.append_header("x-custom", "second"); + + let http_req = from_fastly_headers_ref(&fastly_req); + let values: Vec<_> = http_req + .headers() + .get_all("x-custom") + .iter() + .map(|value| value.to_str().expect("should be valid utf8")) + .collect(); + + assert_eq!( + values, + vec!["first", "second"], + "should preserve duplicates" + ); + } + + #[test] + fn from_fastly_headers_ref_body_is_empty() { + let fastly_req = fastly::Request::new(fastly::http::Method::POST, "https://example.com/"); + + let http_req = from_fastly_headers_ref(&fastly_req); + + assert_eq!(http_req.method(), http::Method::POST, "should copy method"); + assert_once_body_eq(http_req.into_body(), b""); + } + + #[test] + fn to_fastly_response_copies_status_and_headers() { + let http_resp = http::Response::builder() + .status(201) + .header("content-type", "application/json") + .body(EdgeBody::from(b"{}".as_ref())) + .expect("should build response"); + + let fastly_resp = to_fastly_response(http_resp); + + assert_eq!(fastly_resp.get_status().as_u16(), 201, "should copy status"); + assert!( + fastly_resp.get_header("content-type").is_some(), + "should copy content-type header" + ); + } + + #[test] + fn sanitize_fastly_forwarded_headers_strips_spoofable() { + let mut req = fastly::Request::new(fastly::http::Method::GET, "https://example.com"); + req.set_header("forwarded", "host=evil.com"); + req.set_header("x-forwarded-host", "evil.com"); + req.set_header("x-forwarded-proto", "https"); + req.set_header("fastly-ssl", "1"); + req.set_header("host", "legit.example.com"); + + sanitize_fastly_forwarded_headers(&mut req); + + assert!( + req.get_header("forwarded").is_none(), + "should strip Forwarded" + ); + assert!( + req.get_header("x-forwarded-host").is_none(), + "should strip X-Forwarded-Host" + ); + assert!( + req.get_header("x-forwarded-proto").is_none(), + "should strip X-Forwarded-Proto" + ); + assert!( + req.get_header("fastly-ssl").is_none(), + "should strip Fastly-SSL" + ); + assert_eq!( + req.get_header("host").and_then(|v| v.to_str().ok()), + Some("legit.example.com"), + "should preserve Host" + ); + } + + #[test] + fn forward_fastly_cookie_header_strips_consent() { + let mut from_req = fastly::Request::new(fastly::http::Method::GET, "https://example.com"); + from_req.set_header(header::COOKIE, "euconsent-v2=BOE; session=abc"); + let mut to_req = fastly::Request::new(fastly::http::Method::GET, "https://partner.com"); + + forward_fastly_cookie_header(&from_req, &mut to_req, true); + + let forwarded = to_req + .get_header(header::COOKIE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + assert!( + !forwarded.contains("euconsent-v2"), + "should strip consent cookie" + ); + assert!( + forwarded.contains("session=abc"), + "should keep non-consent cookie" + ); + } + + #[test] + fn copy_fastly_custom_headers_filters_internal() { + let mut from_req = fastly::Request::new(fastly::http::Method::GET, "https://example.com"); + from_req.set_header("x-custom-data", "present"); + from_req.set_header("x-ts-ec", "should-not-copy"); + let mut to_req = fastly::Request::new(fastly::http::Method::GET, "https://partner.com"); + + copy_fastly_custom_headers(&from_req, &mut to_req); + + assert_eq!( + to_req + .get_header("x-custom-data") + .and_then(|v| v.to_str().ok()), + Some("present"), + "should copy arbitrary x-header" + ); + assert!( + to_req.get_header("x-ts-ec").is_none(), + "should not copy internal header" + ); + } + + #[test] + fn copy_fastly_custom_headers_preserves_duplicate_values() { + let mut from_req = fastly::Request::new(fastly::http::Method::GET, "https://example.com"); + from_req.append_header("x-custom-data", "first"); + from_req.append_header("x-custom-data", "second"); + let mut to_req = fastly::Request::new(fastly::http::Method::GET, "https://partner.com"); + + copy_fastly_custom_headers(&from_req, &mut to_req); + + let values: Vec<_> = to_req + .get_headers() + .filter(|(name, _)| name.as_str() == "x-custom-data") + .map(|(_, value)| value.to_str().expect("should be valid utf8")) + .collect(); + assert_eq!( + values, + vec!["first", "second"], + "should preserve duplicates" + ); + } + + #[test] + fn set_fastly_ec_cookie_sets_cookie_header() { + let settings = crate::test_support::tests::create_test_settings(); + let mut response = fastly::Response::new(); + + set_fastly_ec_cookie(&settings, &mut response, "abc123.XyZ789"); + + let cookie = response + .get_header(header::SET_COOKIE) + .and_then(|value| value.to_str().ok()) + .map(str::to_owned); + assert_eq!( + cookie, + Some(format!( + "ts-ec=abc123.XyZ789; Domain={}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=31536000", + settings.publisher.cookie_domain + )), + "should set expected EC cookie" + ); + } + + #[test] + fn expire_fastly_ec_cookie_sets_expiry_cookie() { + let settings = crate::test_support::tests::create_test_settings(); + let mut response = fastly::Response::new(); + + expire_fastly_ec_cookie(&settings, &mut response); + + let cookie = response + .get_header(header::SET_COOKIE) + .and_then(|value| value.to_str().ok()) + .map(str::to_owned); + assert_eq!( + cookie, + Some(format!( + "ts-ec=; Domain={}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=0", + settings.publisher.cookie_domain + )), + "should set expected expiry cookie" + ); + } +} diff --git a/crates/trusted-server-core/src/consent/extraction.rs b/crates/trusted-server-core/src/consent/extraction.rs index d5b420bff..98633baac 100644 --- a/crates/trusted-server-core/src/consent/extraction.rs +++ b/crates/trusted-server-core/src/consent/extraction.rs @@ -5,7 +5,8 @@ //! pipeline described in the [Consent Forwarding Architecture Design]. use cookie::CookieJar; -use fastly::Request; +use edgezero_core::body::Body as EdgeBody; +use http::Request; use crate::constants::{ COOKIE_EUCONSENT_V2, COOKIE_GPP, COOKIE_GPP_SID, COOKIE_US_PRIVACY, HEADER_SEC_GPC, @@ -24,7 +25,10 @@ use super::types::RawConsentSignals; /// Also reads the `Sec-GPC` header for Global Privacy Control. /// /// No decoding or validation is performed — values are captured as-is. -pub fn extract_consent_signals(jar: Option<&CookieJar>, req: &Request) -> RawConsentSignals { +pub fn extract_consent_signals( + jar: Option<&CookieJar>, + req: &Request, +) -> RawConsentSignals { let raw_tc_string = jar .and_then(|j| j.get(COOKIE_EUCONSENT_V2)) .map(|c| c.value().to_owned()); @@ -42,7 +46,8 @@ pub fn extract_consent_signals(jar: Option<&CookieJar>, req: &Request) -> RawCon .map(|c| c.value().to_owned()); let gpc = req - .get_header(HEADER_SEC_GPC) + .headers() + .get(HEADER_SEC_GPC) .and_then(|v| v.to_str().ok()) .map(|v| v.trim() == "1") .unwrap_or(false); @@ -61,9 +66,19 @@ mod tests { use super::*; use crate::cookies::parse_cookies_to_jar; + fn build_request(gpc: Option<&str>) -> Request { + let mut builder = Request::builder().method("GET").uri("https://example.com"); + if let Some(value) = gpc { + builder = builder.header("sec-gpc", value); + } + builder + .body(EdgeBody::empty()) + .expect("should build request") + } + #[test] fn no_cookies_no_headers() { - let req = Request::get("https://example.com"); + let req = build_request(None); let signals = extract_consent_signals(None, &req); assert!(signals.is_empty(), "should produce empty signals"); } @@ -71,7 +86,7 @@ mod tests { #[test] fn extracts_euconsent_v2() { let jar = parse_cookies_to_jar("euconsent-v2=CPXxGfAPXxGfAAHABBENBCCsAP_AAH_AAAAAHftf"); - let req = Request::get("https://example.com"); + let req = build_request(None); let signals = extract_consent_signals(Some(&jar), &req); assert_eq!( @@ -84,7 +99,7 @@ mod tests { #[test] fn extracts_gpp_cookies() { let jar = parse_cookies_to_jar("__gpp=DBACNYA~CPXxGfA; __gpp_sid=2,6"); - let req = Request::get("https://example.com"); + let req = build_request(None); let signals = extract_consent_signals(Some(&jar), &req); assert_eq!( @@ -102,7 +117,7 @@ mod tests { #[test] fn extracts_us_privacy() { let jar = parse_cookies_to_jar("us_privacy=1YNN"); - let req = Request::get("https://example.com"); + let req = build_request(None); let signals = extract_consent_signals(Some(&jar), &req); assert_eq!( @@ -114,7 +129,7 @@ mod tests { #[test] fn extracts_sec_gpc_header() { - let req = Request::get("https://example.com").with_header("sec-gpc", "1"); + let req = build_request(Some("1")); let signals = extract_consent_signals(None, &req); assert!(signals.gpc, "should detect Sec-GPC: 1 header"); @@ -122,7 +137,7 @@ mod tests { #[test] fn sec_gpc_absent_when_not_set() { - let req = Request::get("https://example.com"); + let req = build_request(None); let signals = extract_consent_signals(None, &req); assert!(!signals.gpc, "should default gpc to false"); @@ -130,7 +145,7 @@ mod tests { #[test] fn sec_gpc_absent_when_not_one() { - let req = Request::get("https://example.com").with_header("sec-gpc", "0"); + let req = build_request(Some("0")); let signals = extract_consent_signals(None, &req); assert!(!signals.gpc, "should not treat Sec-GPC: 0 as opt-out"); @@ -140,7 +155,7 @@ mod tests { fn extracts_all_signals() { let jar = parse_cookies_to_jar("euconsent-v2=CPXxGf; __gpp=DBAC; __gpp_sid=2,6; us_privacy=1YNN"); - let req = Request::get("https://example.com").with_header("sec-gpc", "1"); + let req = build_request(Some("1")); let signals = extract_consent_signals(Some(&jar), &req); assert!(signals.raw_tc_string.is_some(), "should have tc_string"); @@ -153,7 +168,7 @@ mod tests { #[test] fn empty_jar_produces_no_cookie_signals() { let jar = parse_cookies_to_jar(""); - let req = Request::get("https://example.com"); + let req = build_request(None); let signals = extract_consent_signals(Some(&jar), &req); assert!( @@ -169,7 +184,7 @@ mod tests { #[test] fn unrelated_cookies_ignored() { let jar = parse_cookies_to_jar("session_id=abc123; theme=dark"); - let req = Request::get("https://example.com"); + let req = build_request(None); let signals = extract_consent_signals(Some(&jar), &req); assert!( diff --git a/crates/trusted-server-core/src/consent/mod.rs b/crates/trusted-server-core/src/consent/mod.rs index 9d6050088..36e7e6282 100644 --- a/crates/trusted-server-core/src/consent/mod.rs +++ b/crates/trusted-server-core/src/consent/mod.rs @@ -43,7 +43,8 @@ pub use types::{ use std::time::{SystemTime, UNIX_EPOCH}; use cookie::CookieJar; -use fastly::Request; +use edgezero_core::body::Body as EdgeBody; +use http::Request; use crate::consent_config::{ConflictMode, ConsentConfig, ConsentMode}; use crate::geo::GeoInfo; @@ -62,7 +63,7 @@ pub struct ConsentPipelineInput<'a> { /// Parsed cookie jar from the incoming request. pub jar: Option<&'a CookieJar>, /// The incoming HTTP request (for header access). - pub req: &'a Request, + pub req: &'a Request, /// Publisher consent configuration. pub config: &'a ConsentConfig, /// Geolocation data from the request (for jurisdiction detection). @@ -612,7 +613,8 @@ fn log_consent_context(ctx: &ConsentContext) { #[cfg(test)] mod tests { - use fastly::Request; + use edgezero_core::body::Body as EdgeBody; + use http::Request; use super::{ allows_ec_creation, apply_expiration_check, apply_tcf_conflict_resolution, @@ -626,6 +628,14 @@ mod tests { use crate::consent_config::{ConflictMode, ConsentConfig, ConsentMode}; use crate::cookies::parse_cookies_to_jar; + fn build_request() -> Request { + Request::builder() + .method("GET") + .uri("https://example.com") + .body(EdgeBody::empty()) + .expect("should build consent test request") + } + /// Builder for [`TcfConsent`] test fixtures with sensible defaults. /// /// All purposes default to `false`, timestamps to `0`, and vendor lists @@ -739,7 +749,7 @@ mod tests { #[test] fn proxy_mode_marks_gdpr_when_raw_tc_exists() { let jar = parse_cookies_to_jar("euconsent-v2=CPXxGfAPXxGfA"); - let req = Request::get("https://example.com"); + let req = build_request(); let config = ConsentConfig { mode: ConsentMode::Proxy, ..ConsentConfig::default() @@ -769,7 +779,7 @@ mod tests { #[test] fn proxy_mode_marks_gdpr_when_gpp_sid_contains_tcf_section() { let jar = parse_cookies_to_jar("__gpp_sid=2,6"); - let req = Request::get("https://example.com"); + let req = build_request(); let config = ConsentConfig { mode: ConsentMode::Proxy, ..ConsentConfig::default() diff --git a/crates/trusted-server-core/src/cookies.rs b/crates/trusted-server-core/src/cookies.rs index 963bf1256..3ff9df5e5 100644 --- a/crates/trusted-server-core/src/cookies.rs +++ b/crates/trusted-server-core/src/cookies.rs @@ -6,9 +6,11 @@ use std::borrow::Cow; use cookie::{Cookie, CookieJar}; +use edgezero_core::body::Body as EdgeBody; use error_stack::{Report, ResultExt}; -use fastly::http::header; -use fastly::Request; +use http::header; +use http::Request; +use http::Response; use crate::constants::{ COOKIE_EUCONSENT_V2, COOKIE_GPP, COOKIE_GPP_SID, COOKIE_TS_EC, COOKIE_US_PRIVACY, @@ -65,7 +67,7 @@ fn sanitize_ec_id_for_cookie(ec_id: &str) -> Cow<'_, str> { Cow::Owned(safe_id) } -fn ec_cookie_attributes(settings: &Settings, max_age: i32) -> String { +pub(crate) fn ec_cookie_attributes(settings: &Settings, max_age: i32) -> String { format!( "Domain={}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age={max_age}", settings.publisher.cookie_domain, @@ -97,9 +99,9 @@ pub fn parse_cookies_to_jar(s: &str) -> CookieJar { /// /// - [`TrustedServerError::InvalidHeaderValue`] if the Cookie header contains invalid UTF-8 pub fn handle_request_cookies( - req: &Request, + req: &Request, ) -> Result, Report> { - match req.get_header(header::COOKIE) { + match req.headers().get(header::COOKIE) { Some(header_value) => { let header_value_str = header_value @@ -144,28 +146,35 @@ pub fn strip_cookies(cookie_header: &str, cookie_names: &[&str]) -> String { /// stripping consent cookies. /// /// When `strip_consent` is `true`, cookies listed in [`CONSENT_COOKIE_NAMES`] -/// are removed before forwarding. If stripping leaves no cookies, the header -/// is omitted entirely. Non-UTF-8 cookie headers are forwarded unchanged. -pub fn forward_cookie_header(from: &Request, to: &mut Request, strip_consent: bool) { - let Some(cookie_value) = from.get_header(header::COOKIE) else { - return; - }; - - if !strip_consent { - to.set_header(header::COOKIE, cookie_value); - return; - } +/// are removed before forwarding. If stripping leaves no cookies or yields an +/// invalid header value, the stripped header is omitted. Non-UTF-8 cookie +/// headers are forwarded unchanged. +pub fn forward_cookie_header( + from: &Request, + to: &mut Request, + strip_consent: bool, +) { + for cookie_value in from.headers().get_all(header::COOKIE) { + if !strip_consent { + to.headers_mut() + .append(header::COOKIE, cookie_value.clone()); + continue; + } - match cookie_value.to_str() { - Ok(s) => { - let stripped = strip_cookies(s, CONSENT_COOKIE_NAMES); - if !stripped.is_empty() { - to.set_header(header::COOKIE, &stripped); + match cookie_value.to_str() { + Ok(s) => { + let stripped = strip_cookies(s, CONSENT_COOKIE_NAMES); + if !stripped.is_empty() { + if let Ok(value) = http::HeaderValue::from_str(&stripped) { + to.headers_mut().append(header::COOKIE, value); + } + } + } + Err(_) => { + // Non-UTF-8 Cookie header — forward as-is + to.headers_mut() + .append(header::COOKIE, cookie_value.clone()); } - } - Err(_) => { - // Non-UTF-8 Cookie header — forward as-is - to.set_header(header::COOKIE, cookie_value); } } } @@ -181,7 +190,7 @@ pub fn forward_cookie_header(from: &Request, to: &mut Request, strip_consent: bo /// Non-ASCII characters (multi-byte UTF-8) are always rejected because their /// byte values exceed `0x7E`. #[must_use] -fn is_safe_cookie_value(value: &str) -> bool { +pub(crate) fn ec_cookie_value_is_safe(value: &str) -> bool { // RFC 6265 §4.1.1 cookie-octet: // 0x21 — '!' // 0x23–0x2B — '#' through '+' (excludes 0x22 DQUOTE) @@ -246,24 +255,49 @@ pub fn create_ec_cookie(settings: &Settings, ec_id: &str) -> String { /// from injecting spurious cookie attributes via a controlled ID value. /// /// `cookie_domain` comes from operator configuration and is considered trusted. -pub fn set_ec_cookie(settings: &Settings, response: &mut fastly::Response, ec_id: &str) { - if !is_safe_cookie_value(ec_id) { +/// +/// # Panics +/// +/// Does not panic in practice — the cookie value is validated by +/// [`ec_cookie_value_is_safe`] (early return if invalid) before +/// [`http::HeaderValue::from_str`] is called, so the expect is unreachable. +/// Listed here only because clippy cannot prove it statically. +pub fn set_ec_cookie(settings: &Settings, response: &mut Response, ec_id: &str) { + if !ec_cookie_value_is_safe(ec_id) { log::warn!( "Rejecting EC ID for Set-Cookie: value of {} bytes contains characters illegal in a cookie value", ec_id.len() ); return; } - response.append_header(header::SET_COOKIE, create_ec_cookie(settings, ec_id)); + response.headers_mut().append( + header::SET_COOKIE, + http::HeaderValue::from_str(&create_ec_cookie(settings, ec_id)) + .expect("should build Set-Cookie header value"), + ); } /// Expires the EC cookie by setting `Max-Age=0`. /// /// Used when a user revokes consent — the browser will delete the cookie /// on receipt of this header. -pub fn expire_ec_cookie(settings: &Settings, response: &mut fastly::Response) { - let cookie = format!("{}=; {}", COOKIE_TS_EC, ec_cookie_attributes(settings, 0),); - response.append_header(header::SET_COOKIE, cookie); +/// +/// # Panics +/// +/// Does not panic in practice — the formatted value contains only ASCII +/// printable characters (constant name, validated domain, static attributes), +/// so [`http::HeaderValue::from_str`] always succeeds. Listed here only +/// because clippy cannot prove it statically. +pub fn expire_ec_cookie(settings: &Settings, response: &mut Response) { + response.headers_mut().append( + header::SET_COOKIE, + http::HeaderValue::from_str(&format!( + "{}=; {}", + COOKIE_TS_EC, + ec_cookie_attributes(settings, 0), + )) + .expect("should build expiry Set-Cookie header value"), + ); } #[cfg(test)] @@ -272,6 +306,23 @@ mod tests { use super::*; + fn build_response() -> Response { + Response::builder() + .status(200) + .body(EdgeBody::empty()) + .expect("should build test response") + } + + fn build_request(cookie_header: Option<&str>) -> Request { + let mut builder = Request::builder().method("GET").uri("http://example.com"); + if let Some(cookie_header) = cookie_header { + builder = builder.header(header::COOKIE, cookie_header); + } + builder + .body(EdgeBody::empty()) + .expect("should build test request") + } + #[test] fn test_parse_cookies_to_jar() { let header_value = "c1=v1; c2=v2"; @@ -309,7 +360,7 @@ mod tests { #[test] fn test_handle_request_cookies() { - let req = Request::get("http://example.com").with_header(header::COOKIE, "c1=v1;c2=v2"); + let req = build_request(Some("c1=v1;c2=v2")); let jar = handle_request_cookies(&req) .expect("should parse cookies") .expect("should have cookie jar"); @@ -321,7 +372,7 @@ mod tests { #[test] fn test_handle_request_cookies_with_empty_cookie() { - let req = Request::get("http://example.com").with_header(header::COOKIE, ""); + let req = build_request(Some("")); let jar = handle_request_cookies(&req) .expect("should parse cookies") .expect("should have cookie jar"); @@ -331,7 +382,7 @@ mod tests { #[test] fn test_handle_request_cookies_no_cookie_header() { - let req: Request = Request::get("https://example.com"); + let req = build_request(None); let jar = handle_request_cookies(&req).expect("should handle missing cookie header"); assert!(jar.is_none()); @@ -339,7 +390,7 @@ mod tests { #[test] fn test_handle_request_cookies_invalid_cookie_header() { - let req = Request::get("http://example.com").with_header(header::COOKIE, "invalid"); + let req = build_request(Some("invalid")); let jar = handle_request_cookies(&req) .expect("should parse cookies") .expect("should have cookie jar"); @@ -350,11 +401,12 @@ mod tests { #[test] fn test_set_ec_cookie() { let settings = create_test_settings(); - let mut response = fastly::Response::new(); + let mut response = build_response(); set_ec_cookie(&settings, &mut response, "abc123.XyZ789"); let cookie_str = response - .get_header(header::SET_COOKIE) + .headers() + .get(header::SET_COOKIE) .expect("Set-Cookie header should be present") .to_str() .expect("header should be valid UTF-8"); @@ -403,11 +455,11 @@ mod tests { #[test] fn test_set_ec_cookie_rejects_semicolon() { let settings = create_test_settings(); - let mut response = fastly::Response::new(); + let mut response = build_response(); set_ec_cookie(&settings, &mut response, "evil; Domain=.attacker.com"); assert!( - response.get_header(header::SET_COOKIE).is_none(), + response.headers().get(header::SET_COOKIE).is_none(), "Set-Cookie should not be set when value contains a semicolon" ); } @@ -415,11 +467,11 @@ mod tests { #[test] fn test_set_ec_cookie_rejects_crlf() { let settings = create_test_settings(); - let mut response = fastly::Response::new(); + let mut response = build_response(); set_ec_cookie(&settings, &mut response, "evil\r\nX-Injected: header"); assert!( - response.get_header(header::SET_COOKIE).is_none(), + response.headers().get(header::SET_COOKIE).is_none(), "Set-Cookie should not be set when value contains CRLF" ); } @@ -427,25 +479,25 @@ mod tests { #[test] fn test_set_ec_cookie_rejects_space() { let settings = create_test_settings(); - let mut response = fastly::Response::new(); + let mut response = build_response(); set_ec_cookie(&settings, &mut response, "bad value"); assert!( - response.get_header(header::SET_COOKIE).is_none(), + response.headers().get(header::SET_COOKIE).is_none(), "Set-Cookie should not be set when value contains whitespace" ); } #[test] fn test_is_safe_cookie_value_rejects_empty_string() { - assert!(!is_safe_cookie_value(""), "should reject empty string"); + assert!(!ec_cookie_value_is_safe(""), "should reject empty string"); } #[test] fn test_is_safe_cookie_value_accepts_valid_ec_id_characters() { // Hex digits, dot separator, alphanumeric suffix — the full EC ID character set assert!( - is_safe_cookie_value("abcdef0123456789.ABCDEFabcdef"), + ec_cookie_value_is_safe("abcdef0123456789.ABCDEFabcdef"), "should accept hex digits, dots, and alphanumeric characters" ); } @@ -453,27 +505,33 @@ mod tests { #[test] fn test_is_safe_cookie_value_rejects_non_ascii() { assert!( - !is_safe_cookie_value("valüe"), + !ec_cookie_value_is_safe("valüe"), "should reject non-ASCII UTF-8 characters" ); } #[test] fn test_is_safe_cookie_value_rejects_illegal_characters() { - assert!(!is_safe_cookie_value("val;ue"), "should reject semicolon"); - assert!(!is_safe_cookie_value("val,ue"), "should reject comma"); assert!( - !is_safe_cookie_value("val\"ue"), + !ec_cookie_value_is_safe("val;ue"), + "should reject semicolon" + ); + assert!(!ec_cookie_value_is_safe("val,ue"), "should reject comma"); + assert!( + !ec_cookie_value_is_safe("val\"ue"), "should reject double-quote" ); - assert!(!is_safe_cookie_value("val\\ue"), "should reject backslash"); - assert!(!is_safe_cookie_value("val ue"), "should reject space"); assert!( - !is_safe_cookie_value("val\x00ue"), + !ec_cookie_value_is_safe("val\\ue"), + "should reject backslash" + ); + assert!(!ec_cookie_value_is_safe("val ue"), "should reject space"); + assert!( + !ec_cookie_value_is_safe("val\x00ue"), "should reject null byte" ); assert!( - !is_safe_cookie_value("val\x7fue"), + !ec_cookie_value_is_safe("val\x7fue"), "should reject DEL character" ); } @@ -481,12 +539,13 @@ mod tests { #[test] fn test_expire_ec_cookie_matches_security_attributes() { let settings = create_test_settings(); - let mut response = fastly::Response::new(); + let mut response = build_response(); expire_ec_cookie(&settings, &mut response); let cookie_header = response - .get_header(header::SET_COOKIE) + .headers() + .get(header::SET_COOKIE) .expect("Set-Cookie header should be present"); let cookie_str = cookie_header .to_str() @@ -502,6 +561,111 @@ mod tests { ); } + // --------------------------------------------------------------- + // forward_cookie_header tests + // --------------------------------------------------------------- + + #[test] + fn test_forward_cookie_header_strips_consent() { + let from = build_request(Some("euconsent-v2=BOE; session=abc123; us_privacy=1YNN")); + let mut to = build_request(None); + + forward_cookie_header(&from, &mut to, true); + + let forwarded = to + .headers() + .get(header::COOKIE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + assert!( + !forwarded.contains("euconsent-v2"), + "should strip consent cookie" + ); + assert!( + forwarded.contains("session=abc123"), + "should keep non-consent cookie" + ); + } + + #[test] + fn test_forward_cookie_header_strip_all_leaves_header_absent() { + let from = build_request(Some("euconsent-v2=BOE; __gpp=DBAC")); + let mut to = build_request(None); + + forward_cookie_header(&from, &mut to, true); + + assert!( + to.headers().get(header::COOKIE).is_none(), + "should omit Cookie header when all cookies are stripped" + ); + } + + #[test] + fn test_forward_cookie_header_no_strip_passes_all() { + let from = build_request(Some("euconsent-v2=BOE; session=abc123")); + let mut to = build_request(None); + + forward_cookie_header(&from, &mut to, false); + + let forwarded = to + .headers() + .get(header::COOKIE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + assert!( + forwarded.contains("euconsent-v2"), + "should forward consent cookie when not stripping" + ); + assert!( + forwarded.contains("session=abc123"), + "should forward non-consent cookie" + ); + } + + #[test] + fn test_forward_cookie_header_non_utf8_forwarded_unchanged() { + let non_utf8 = http::HeaderValue::from_bytes(b"\xff\xfe=value") + .expect("should build non-UTF-8 header value"); + let mut from = build_request(None); + from.headers_mut().append(header::COOKIE, non_utf8); + let mut to = build_request(None); + + forward_cookie_header(&from, &mut to, true); + + let forwarded = to.headers().get(header::COOKIE); + assert!( + forwarded.is_some(), + "should forward non-UTF-8 Cookie header unchanged" + ); + assert_eq!( + forwarded.expect("should have cookie header").as_bytes(), + b"\xff\xfe=value", + "should preserve raw bytes for non-UTF-8 cookie" + ); + } + + #[test] + fn test_forward_cookie_header_multiple_cookie_headers_appended() { + let mut from = build_request(Some("session=abc123")); + from.headers_mut().append( + header::COOKIE, + "theme=dark".parse().expect("should parse header value"), + ); + let mut to = build_request(None); + + forward_cookie_header(&from, &mut to, false); + + let all_cookies: Vec<_> = to + .headers() + .get_all(header::COOKIE) + .iter() + .filter_map(|v| v.to_str().ok()) + .collect(); + assert_eq!(all_cookies.len(), 2, "should append all Cookie headers"); + assert!(all_cookies.iter().any(|v| v.contains("session=abc123"))); + assert!(all_cookies.iter().any(|v| v.contains("theme=dark"))); + } + // --------------------------------------------------------------- // strip_cookies tests // --------------------------------------------------------------- diff --git a/crates/trusted-server-core/src/edge_cookie.rs b/crates/trusted-server-core/src/edge_cookie.rs index 7d2094e35..565d95316 100644 --- a/crates/trusted-server-core/src/edge_cookie.rs +++ b/crates/trusted-server-core/src/edge_cookie.rs @@ -11,6 +11,7 @@ use hmac::{Hmac, Mac}; use rand::Rng; use sha2::Sha256; +use crate::compat; use crate::constants::{COOKIE_TS_EC, HEADER_X_TS_EC}; use crate::cookies::{ec_id_has_only_allowed_chars, handle_request_cookies}; use crate::error::TrustedServerError; @@ -116,7 +117,8 @@ pub fn get_ec_id(req: &Request) -> Result, Report { if let Some(cookie) = jar.get(COOKIE_TS_EC) { let value = cookie.value(); diff --git a/crates/trusted-server-core/src/http_util.rs b/crates/trusted-server-core/src/http_util.rs index 6944fb1c3..d7e61e3ba 100644 --- a/crates/trusted-server-core/src/http_util.rs +++ b/crates/trusted-server-core/src/http_util.rs @@ -1,7 +1,7 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use chacha20poly1305::{aead::Aead, aead::KeyInit, XChaCha20Poly1305, XNonce}; -use fastly::http::{header, StatusCode}; -use fastly::{Request, Response}; +use edgezero_core::body::Body as EdgeBody; +use http::{header, Request, Response, StatusCode}; use sha2::{Digest, Sha256}; use subtle::ConstantTimeEq as _; @@ -15,15 +15,11 @@ use crate::settings::Settings; /// internal identity, geo-enrichment, and debugging data to downstream third-party /// services. Integrations that forward custom headers should use this utility /// instead of manually iterating over header names. -pub fn copy_custom_headers(from: &Request, to: &mut Request) { - for header_name in from.get_header_names() { +pub fn copy_custom_headers(from: &Request, to: &mut Request) { + for (header_name, value) in from.headers() { let name_str = header_name.as_str(); - if (name_str.starts_with("x-") || name_str.starts_with("X-")) - && !INTERNAL_HEADERS.contains(&name_str) - { - if let Some(value) = from.get_header(header_name) { - to.set_header(header_name, value); - } + if name_str.starts_with("x-") && !INTERNAL_HEADERS.contains(&name_str) { + to.headers_mut().append(header_name.clone(), value.clone()); } } } @@ -33,7 +29,7 @@ pub fn copy_custom_headers(from: &Request, to: &mut Request) { /// On Fastly Compute the service is the edge - there is no upstream proxy that /// legitimately sets these. Stripping them forces [`RequestInfo::from_request`] /// to fall back to the trustworthy `Host` header and [`ClientInfo`] TLS detection. -const SPOOFABLE_FORWARDED_HEADERS: &[&str] = &[ +pub(crate) const SPOOFABLE_FORWARDED_HEADERS: &[&str] = &[ "forwarded", "x-forwarded-host", "x-forwarded-proto", @@ -45,11 +41,11 @@ const SPOOFABLE_FORWARDED_HEADERS: &[&str] = &[ /// Call this at the edge entry point (before routing) to prevent /// `X-Forwarded-Host: evil.com` from hijacking all URL rewriting. /// See . -pub fn sanitize_forwarded_headers(req: &mut Request) { +pub fn sanitize_forwarded_headers(req: &mut Request) { for header in SPOOFABLE_FORWARDED_HEADERS { - if req.get_header(*header).is_some() { + if req.headers().contains_key(*header) { log::debug!("Stripped spoofable header: {}", header); - req.remove_header(*header); + req.headers_mut().remove(*header); } } } @@ -88,7 +84,7 @@ impl RequestInfo { /// In production the forwarded headers are stripped by /// [`sanitize_forwarded_headers`] at the edge, so `Host` and /// [`ClientInfo`] TLS detection are the only sources that fire. - pub fn from_request(req: &Request, client_info: &ClientInfo) -> Self { + pub fn from_request(req: &Request, client_info: &ClientInfo) -> Self { let host = extract_request_host(req); let scheme = detect_request_scheme( req, @@ -100,16 +96,22 @@ impl RequestInfo { } } -fn extract_request_host(req: &Request) -> String { - req.get_header("forwarded") +fn extract_request_host(req: &Request) -> String { + req.headers() + .get("forwarded") .and_then(|h| h.to_str().ok()) .and_then(|value| parse_forwarded_param(value, "host")) .or_else(|| { - req.get_header("x-forwarded-host") + req.headers() + .get("x-forwarded-host") .and_then(|h| h.to_str().ok()) .and_then(parse_list_header_value) }) - .or_else(|| req.get_header(header::HOST).and_then(|h| h.to_str().ok())) + .or_else(|| { + req.headers() + .get(header::HOST) + .and_then(|h| h.to_str().ok()) + }) .unwrap_or_default() .to_string() } @@ -170,7 +172,7 @@ fn normalize_scheme(value: &str) -> Option { /// 4. Fastly-SSL header (least reliable, can be spoofed) /// 5. Default to HTTP fn detect_request_scheme( - req: &Request, + req: &Request, tls_protocol: Option<&str>, tls_cipher: Option<&str>, ) -> String { @@ -187,7 +189,7 @@ fn detect_request_scheme( } // 2. Try the Forwarded header (RFC 7239) - if let Some(forwarded) = req.get_header("forwarded") { + if let Some(forwarded) = req.headers().get("forwarded") { if let Ok(forwarded_str) = forwarded.to_str() { if let Some(proto) = parse_forwarded_param(forwarded_str, "proto") { if let Some(scheme) = normalize_scheme(proto) { @@ -198,7 +200,7 @@ fn detect_request_scheme( } // 3. Try X-Forwarded-Proto header - if let Some(proto) = req.get_header("x-forwarded-proto") { + if let Some(proto) = req.headers().get("x-forwarded-proto") { if let Ok(proto_str) = proto.to_str() { if let Some(value) = parse_list_header_value(proto_str) { if let Some(scheme) = normalize_scheme(value) { @@ -209,7 +211,7 @@ fn detect_request_scheme( } // 4. Check Fastly-SSL header (can be spoofed by clients, use as last resort) - if let Some(ssl) = req.get_header("fastly-ssl") { + if let Some(ssl) = req.headers().get("fastly-ssl") { if let Ok(ssl_str) = ssl.to_str() { if ssl_str == "1" || ssl_str.to_lowercase() == "true" { return "https".to_string(); @@ -223,38 +225,53 @@ fn detect_request_scheme( /// Build a static text response with strong `ETag` and standard caching headers. /// Handles If-None-Match to return 304 when appropriate. -pub fn serve_static_with_etag(body: &str, req: &Request, content_type: &str) -> Response { +/// +/// # Panics +/// +/// Panics if the generated response headers cannot be represented in an +/// `http::Response`. +pub fn serve_static_with_etag( + body: &str, + req: &Request, + content_type: &str, +) -> Response { // Compute ETag for conditional caching let hash = Sha256::digest(body.as_bytes()); let etag = format!("\"sha256-{}\"", hex::encode(hash)); // If-None-Match handling for 304 responses if let Some(if_none_match) = req - .get_header(header::IF_NONE_MATCH) + .headers() + .get(header::IF_NONE_MATCH) .and_then(|h| h.to_str().ok()) { if if_none_match == etag { - return Response::from_status(StatusCode::NOT_MODIFIED) - .with_header(header::ETAG, &etag) - .with_header( + return Response::builder() + .status(StatusCode::NOT_MODIFIED) + .header(header::ETAG, &etag) + .header( header::CACHE_CONTROL, "public, max-age=300, s-maxage=300, stale-while-revalidate=60, stale-if-error=86400", ) - .with_header("surrogate-control", "max-age=300") - .with_header(header::VARY, "Accept-Encoding"); + .header("surrogate-control", "max-age=300") + .header(header::VARY, "Accept-Encoding") + .body(EdgeBody::empty()) + .expect("should build 304 static response"); } } - Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, content_type) - .with_header( + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, content_type) + .header( header::CACHE_CONTROL, "public, max-age=300, s-maxage=300, stale-while-revalidate=60, stale-if-error=86400", ) - .with_header("surrogate-control", "max-age=300") - .with_header(header::ETAG, &etag) - .with_header(header::VARY, "Accept-Encoding") - .with_body(body) + .header("surrogate-control", "max-age=300") + .header(header::ETAG, &etag) + .header(header::VARY, "Accept-Encoding") + .body(EdgeBody::from(body.as_bytes())) + .expect("should build static response") } /// Encrypts a URL using XChaCha20-Poly1305 with a key derived from the publisher `proxy_secret`. @@ -386,6 +403,22 @@ pub fn compute_encrypted_sha256_token(settings: &Settings, full_url: &str) -> St mod tests { use super::*; use crate::platform::ClientInfo; + use http::{HeaderName, HeaderValue, Method}; + + fn build_request(method: Method, uri: &str) -> Request { + Request::builder() + .method(method) + .uri(uri) + .body(EdgeBody::empty()) + .expect("should build request") + } + + fn set_header(req: &mut Request, name: &str, value: &str) { + req.headers_mut().insert( + HeaderName::from_bytes(name.as_bytes()).expect("should build header name"), + HeaderValue::from_str(value).expect("should build header value"), + ); + } #[test] fn encode_decode_roundtrip() { @@ -454,8 +487,8 @@ mod tests { #[test] fn test_request_info_from_host_header() { - let mut req = Request::new(fastly::http::Method::GET, "https://test.example.com/page"); - req.set_header("host", "test.example.com"); + let mut req = build_request(Method::GET, "https://test.example.com/page"); + set_header(&mut req, "host", "test.example.com"); let info = RequestInfo::from_request( &req, @@ -478,9 +511,13 @@ mod tests { #[test] fn test_request_info_x_forwarded_host_precedence() { - let mut req = Request::new(fastly::http::Method::GET, "https://test.example.com/page"); - req.set_header("host", "internal-proxy.local"); - req.set_header("x-forwarded-host", "public.example.com, proxy.local"); + let mut req = build_request(Method::GET, "https://test.example.com/page"); + set_header(&mut req, "host", "internal-proxy.local"); + set_header( + &mut req, + "x-forwarded-host", + "public.example.com, proxy.local", + ); let info = RequestInfo::from_request( &req, @@ -498,9 +535,9 @@ mod tests { #[test] fn test_request_info_scheme_from_x_forwarded_proto() { - let mut req = Request::new(fastly::http::Method::GET, "https://test.example.com/page"); - req.set_header("host", "test.example.com"); - req.set_header("x-forwarded-proto", "https, http"); + let mut req = build_request(Method::GET, "https://test.example.com/page"); + set_header(&mut req, "host", "test.example.com"); + set_header(&mut req, "x-forwarded-proto", "https, http"); let info = RequestInfo::from_request( &req, @@ -516,9 +553,9 @@ mod tests { ); // Test HTTP - let mut req = Request::new(fastly::http::Method::GET, "http://test.example.com/page"); - req.set_header("host", "test.example.com"); - req.set_header("x-forwarded-proto", "http"); + let mut req = build_request(Method::GET, "http://test.example.com/page"); + set_header(&mut req, "host", "test.example.com"); + set_header(&mut req, "x-forwarded-proto", "http"); let info = RequestInfo::from_request( &req, @@ -537,14 +574,15 @@ mod tests { #[test] fn request_info_forwarded_header_precedence() { // Forwarded header takes precedence over X-Forwarded-Proto - let mut req = Request::new(fastly::http::Method::GET, "https://test.example.com/page"); - req.set_header( + let mut req = build_request(Method::GET, "https://test.example.com/page"); + set_header( + &mut req, "forwarded", "for=192.0.2.60;proto=\"HTTPS\";host=\"public.example.com:443\"", ); - req.set_header("host", "internal-proxy.local"); - req.set_header("x-forwarded-host", "proxy.local"); - req.set_header("x-forwarded-proto", "http"); + set_header(&mut req, "host", "internal-proxy.local"); + set_header(&mut req, "x-forwarded-host", "proxy.local"); + set_header(&mut req, "x-forwarded-proto", "http"); let info = RequestInfo::from_request( &req, @@ -566,8 +604,8 @@ mod tests { #[test] fn test_request_info_scheme_from_fastly_ssl() { - let mut req = Request::new(fastly::http::Method::GET, "https://test.example.com/page"); - req.set_header("fastly-ssl", "1"); + let mut req = build_request(Method::GET, "https://test.example.com/page"); + set_header(&mut req, "fastly-ssl", "1"); let info = RequestInfo::from_request( &req, @@ -587,13 +625,10 @@ mod tests { fn test_request_info_chained_proxy_scenario() { // Simulate: Client (HTTPS) -> Proxy A -> Trusted Server (HTTP internally) // Proxy A sets X-Forwarded-Host and X-Forwarded-Proto - let mut req = Request::new( - fastly::http::Method::GET, - "http://trusted-server.internal/page", - ); - req.set_header("host", "trusted-server.internal"); - req.set_header("x-forwarded-host", "public.example.com"); - req.set_header("x-forwarded-proto", "https"); + let mut req = build_request(Method::GET, "http://trusted-server.internal/page"); + set_header(&mut req, "host", "trusted-server.internal"); + set_header(&mut req, "x-forwarded-host", "public.example.com"); + set_header(&mut req, "x-forwarded-proto", "https"); let info = RequestInfo::from_request( &req, @@ -617,33 +652,34 @@ mod tests { #[test] fn sanitize_removes_all_spoofable_headers() { - let mut req = Request::new(fastly::http::Method::GET, "https://example.com/page"); - req.set_header("host", "legit.example.com"); - req.set_header("forwarded", "host=evil.com;proto=https"); - req.set_header("x-forwarded-host", "evil.com"); - req.set_header("x-forwarded-proto", "https"); - req.set_header("fastly-ssl", "1"); + let mut req = build_request(Method::GET, "https://example.com/page"); + set_header(&mut req, "host", "legit.example.com"); + set_header(&mut req, "forwarded", "host=evil.com;proto=https"); + set_header(&mut req, "x-forwarded-host", "evil.com"); + set_header(&mut req, "x-forwarded-proto", "https"); + set_header(&mut req, "fastly-ssl", "1"); sanitize_forwarded_headers(&mut req); assert!( - req.get_header("forwarded").is_none(), + req.headers().get("forwarded").is_none(), "should strip Forwarded header" ); assert!( - req.get_header("x-forwarded-host").is_none(), + req.headers().get("x-forwarded-host").is_none(), "should strip X-Forwarded-Host header" ); assert!( - req.get_header("x-forwarded-proto").is_none(), + req.headers().get("x-forwarded-proto").is_none(), "should strip X-Forwarded-Proto header" ); assert!( - req.get_header("fastly-ssl").is_none(), + req.headers().get("fastly-ssl").is_none(), "should strip Fastly-SSL header" ); assert_eq!( - req.get_header("host") + req.headers() + .get("host") .expect("should have Host header") .to_str() .expect("should be valid UTF-8"), @@ -654,10 +690,10 @@ mod tests { #[test] fn sanitize_then_request_info_falls_back_to_host() { - let mut req = Request::new(fastly::http::Method::GET, "https://example.com/page"); - req.set_header("host", "legit.example.com"); - req.set_header("x-forwarded-host", "evil.com"); - req.set_header("x-forwarded-proto", "http"); + let mut req = build_request(Method::GET, "https://example.com/page"); + set_header(&mut req, "host", "legit.example.com"); + set_header(&mut req, "x-forwarded-host", "evil.com"); + set_header(&mut req, "x-forwarded-proto", "http"); sanitize_forwarded_headers(&mut req); let info = RequestInfo::from_request( @@ -699,39 +735,77 @@ mod tests { #[test] fn test_copy_custom_headers_filters_internal() { - let mut req = Request::new(fastly::http::Method::GET, "https://example.com"); - req.set_header("x-custom-1", "value1"); - // HeaderName is case-insensitive and always lowercase, but set_header accepts strings - req.set_header("X-Custom-2", "value2"); - req.set_header("x-ts-ec", "should not copy"); - req.set_header("x-geo-country", "US"); - - let mut target = Request::new(fastly::http::Method::GET, "https://target.com"); + let mut req = build_request(Method::GET, "https://example.com"); + set_header(&mut req, "x-custom-1", "value1"); + // HeaderName is case-insensitive and normalized by `http`. + set_header(&mut req, "X-Custom-2", "value2"); + set_header(&mut req, "x-ts-ec", "should not copy"); + set_header(&mut req, "x-geo-country", "US"); + + let mut target = build_request(Method::GET, "https://target.com"); copy_custom_headers(&req, &mut target); assert_eq!( - target.get_header("x-custom-1").unwrap().to_str().unwrap(), + target + .headers() + .get("x-custom-1") + .unwrap() + .to_str() + .unwrap(), "value1", "Should copy arbitrary x-header" ); assert_eq!( - target.get_header("x-custom-2").unwrap().to_str().unwrap(), + target + .headers() + .get("x-custom-2") + .unwrap() + .to_str() + .unwrap(), "value2", "Should copy arbitrary X-header (case insensitive)" ); assert!( - target.get_header("x-ts-ec").is_none(), + target.headers().get("x-ts-ec").is_none(), "Should filter x-ts-ec" ); assert!( - target.get_header("x-geo-country").is_none(), + target.headers().get("x-geo-country").is_none(), "Should filter x-geo-country" ); } + #[test] + fn copy_custom_headers_preserves_duplicate_values() { + let mut from = build_request(Method::GET, "https://example.com"); + from.headers_mut().append( + HeaderName::from_bytes(b"x-custom-data").expect("should build header name"), + HeaderValue::from_str("first").expect("should build header value"), + ); + from.headers_mut().append( + HeaderName::from_bytes(b"x-custom-data").expect("should build header name"), + HeaderValue::from_str("second").expect("should build header value"), + ); + + let mut target = build_request(Method::GET, "https://target.com"); + copy_custom_headers(&from, &mut target); + + let values: Vec<_> = target + .headers() + .get_all("x-custom-data") + .iter() + .map(|v| v.to_str().expect("should be valid utf8")) + .collect(); + assert_eq!( + values, + vec!["first", "second"], + "should preserve duplicate x-header values" + ); + } + #[test] fn request_info_https_from_client_info_tls_protocol() { - let req = Request::new(fastly::http::Method::GET, "https://test.example.com/page"); + let req = build_request(Method::GET, "https://test.example.com/page"); let client_info = ClientInfo { client_ip: None, tls_protocol: Some("TLSv1.3".to_string()), @@ -748,7 +822,7 @@ mod tests { #[test] fn request_info_https_from_client_info_tls_cipher() { - let req = Request::new(fastly::http::Method::GET, "https://test.example.com/page"); + let req = build_request(Method::GET, "https://test.example.com/page"); let client_info = ClientInfo { client_ip: None, tls_protocol: None, diff --git a/crates/trusted-server-core/src/integrations/lockr.rs b/crates/trusted-server-core/src/integrations/lockr.rs index ad0091300..8e63345fe 100644 --- a/crates/trusted-server-core/src/integrations/lockr.rs +++ b/crates/trusted-server-core/src/integrations/lockr.rs @@ -17,9 +17,8 @@ use serde::Deserialize; use validator::Validate; use crate::backend::BackendConfig; -use crate::cookies::forward_cookie_header; +use crate::compat; use crate::error::TrustedServerError; -use crate::http_util::copy_custom_headers; use crate::integrations::{ AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter, IntegrationEndpoint, IntegrationProxy, IntegrationRegistration, @@ -236,7 +235,7 @@ impl LockrIntegration { } // Always strip consent cookies — consent travels through the OpenRTB body - forward_cookie_header(from, to, true); + compat::forward_fastly_cookie_header(from, to, true); // Use origin override if configured, otherwise forward original let origin = self @@ -248,7 +247,7 @@ impl LockrIntegration { to.set_header(header::ORIGIN, origin); } - copy_custom_headers(from, to); + compat::copy_fastly_custom_headers(from, to); } } diff --git a/crates/trusted-server-core/src/integrations/permutive.rs b/crates/trusted-server-core/src/integrations/permutive.rs index 0946a54a3..41d7e3bf0 100644 --- a/crates/trusted-server-core/src/integrations/permutive.rs +++ b/crates/trusted-server-core/src/integrations/permutive.rs @@ -13,8 +13,8 @@ use serde::Deserialize; use validator::Validate; use crate::backend::BackendConfig; +use crate::compat; use crate::error::TrustedServerError; -use crate::http_util::copy_custom_headers; use crate::integrations::{ AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter, IntegrationEndpoint, IntegrationProxy, IntegrationRegistration, @@ -495,7 +495,7 @@ impl PermutiveIntegration { } // Copy any X-* custom headers, skipping TS-internal headers - copy_custom_headers(from, to); + compat::copy_fastly_custom_headers(from, to); } } diff --git a/crates/trusted-server-core/src/integrations/prebid.rs b/crates/trusted-server-core/src/integrations/prebid.rs index 6dfd1ca1a..81a91787c 100644 --- a/crates/trusted-server-core/src/integrations/prebid.rs +++ b/crates/trusted-server-core/src/integrations/prebid.rs @@ -15,8 +15,8 @@ use crate::auction::types::{ AuctionContext, AuctionRequest, AuctionResponse, Bid as AuctionBid, MediaType, }; use crate::backend::BackendConfig; +use crate::compat; use crate::consent_config::ConsentForwardingMode; -use crate::cookies::forward_cookie_header; use crate::error::TrustedServerError; use crate::http_util::RequestInfo; use crate::integrations::{ @@ -443,7 +443,7 @@ fn copy_request_headers( } } - forward_cookie_header(from, to, consent_forwarding.strips_consent_cookies()); + compat::forward_fastly_cookie_header(from, to, consent_forwarding.strips_consent_cookies()); } /// Appends query parameters to a URL, handling both URLs with and without existing query strings. @@ -481,6 +481,7 @@ impl PrebidAuctionProvider { request: &AuctionRequest, context: &AuctionContext<'_>, signer: Option<(&RequestSigner, String, &SigningParams)>, + request_info: RequestInfo, ) -> OpenRtbRequest { let imps = request .slots @@ -710,7 +711,6 @@ impl PrebidAuctionProvider { let regs = Self::build_regs(consent_ctx); // Build ext object - let request_info = RequestInfo::from_request(context.request, context.client_info); let (version, signature, kid, ts) = signer .map(|(s, sig, params)| { ( @@ -1003,23 +1003,27 @@ impl AuctionProvider for PrebidAuctionProvider { ) -> Result> { log::info!("Prebid: requesting bids for {} slots", request.slots.len()); + let http_req = compat::from_fastly_headers_ref(context.request); + let request_info = RequestInfo::from_request(&http_req, context.client_info); + // Create signer and compute signature if request signing is enabled - let signer_with_signature = if let Some(request_signing_config) = - &context.settings.request_signing - { - if request_signing_config.enabled { - let request_info = RequestInfo::from_request(context.request, context.client_info); - let signer = RequestSigner::from_services(context.services)?; - let params = - SigningParams::new(request.id.clone(), request_info.host, request_info.scheme); - let signature = signer.sign_request(¶ms)?; - Some((signer, signature, params)) + let signer_with_signature = + if let Some(request_signing_config) = &context.settings.request_signing { + if request_signing_config.enabled { + let signer = RequestSigner::from_services(context.services)?; + let params = SigningParams::new( + request.id.clone(), + request_info.host.clone(), + request_info.scheme.clone(), + ); + let signature = signer.sign_request(¶ms)?; + Some((signer, signature, params)) + } else { + None + } } else { None - } - } else { - None - }; + }; // Convert to OpenRTB with all enrichments let openrtb = self.to_openrtb( @@ -1028,6 +1032,7 @@ impl AuctionProvider for PrebidAuctionProvider { signer_with_signature .as_ref() .map(|(s, sig, params)| (s, sig.clone(), params)), + request_info, ); // An empty `imp` array violates the OpenRTB spec and wastes a network @@ -1298,6 +1303,11 @@ mod tests { shared_test_auction_context(settings, request, client_info, 1000) } + fn make_request_info(context: &AuctionContext<'_>) -> RequestInfo { + let http_req = compat::from_fastly_headers_ref(context.request); + RequestInfo::from_request(&http_req, context.client_info) + } + fn config_from_settings( settings: &Settings, registry: &IntegrationRegistry, @@ -1707,7 +1717,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert_eq!( openrtb.test, None, @@ -1754,7 +1769,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert_eq!( openrtb.test, @@ -1783,7 +1803,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert_eq!( openrtb @@ -1810,7 +1835,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert_eq!( openrtb.test, None, @@ -1860,7 +1890,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let imp = &openrtb.imp[0]; assert_eq!(imp.bidfloor, Some(1.5), "should set bidfloor from slot"); @@ -1880,7 +1915,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let imp = &openrtb.imp[0]; assert_eq!(imp.bidfloor, None, "should omit bidfloor when not set"); @@ -1899,7 +1939,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let imp = &openrtb.imp[0]; assert_eq!(imp.secure, Some(true), "should require HTTPS creatives"); @@ -1938,7 +1983,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert_eq!( openrtb.user.as_ref().and_then(|u| u.consent.as_deref()), @@ -1983,7 +2033,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert_eq!( openrtb.regs.as_ref().and_then(|r| r.gdpr), @@ -2016,7 +2071,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert!( openrtb.regs.is_none(), @@ -2039,7 +2099,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert_eq!( openrtb.regs.as_ref().and_then(|r| r.gdpr), @@ -2057,7 +2122,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert!(openrtb.regs.is_none(), "should omit regs entirely"); } @@ -2078,7 +2148,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let regs = openrtb.regs.as_ref().expect("should have regs"); assert_eq!( @@ -2294,7 +2369,12 @@ server_url = "https://prebid.example" request.set_header("DNT", "1"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let device = openrtb.device.as_ref().expect("should have device"); assert_eq!(device.dnt, Some(true), "should set dnt from DNT header"); @@ -2315,7 +2395,12 @@ server_url = "https://prebid.example" request.set_header("Accept-Language", "en-US,en;q=0.9,fr;q=0.8"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let device = openrtb.device.as_ref().expect("should have device"); assert_eq!( @@ -2340,7 +2425,12 @@ server_url = "https://prebid.example" request.set_header("Accept-Language", ""); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let device = openrtb.device.as_ref().expect("should have device"); assert_eq!( @@ -2371,7 +2461,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert!( openrtb.imp.is_empty(), @@ -2401,7 +2496,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let geo = openrtb .device .as_ref() @@ -2429,7 +2529,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert_eq!( openrtb.tmax, @@ -2454,7 +2559,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); assert_eq!( openrtb.tmax, None, @@ -2476,7 +2586,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let formats = &openrtb.imp[0] .banner .as_ref() @@ -2502,7 +2617,12 @@ server_url = "https://prebid.example" request.set_header("Referer", "https://google.com/search?q=test"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let site = openrtb.site.as_ref().expect("should have site"); assert_eq!( @@ -2521,7 +2641,12 @@ server_url = "https://prebid.example" let request = Request::get("https://pub.example/auction"); let context = create_test_auction_context(&settings, &request, &EMPTY_CLIENT_INFO); - let openrtb = provider.to_openrtb(&auction_request, &context, None); + let openrtb = provider.to_openrtb( + &auction_request, + &context, + None, + make_request_info(&context), + ); let publisher = openrtb .site .as_ref() @@ -2686,7 +2811,8 @@ server_url = "https://prebid.example" provider_responses: None, services: &services, }; - provider.to_openrtb(request, &context, None) + let request_info = make_request_info(&context); + provider.to_openrtb(request, &context, None, request_info) } fn bidder_params(ortb: &OpenRtbRequest) -> &serde_json::Map { diff --git a/crates/trusted-server-core/src/integrations/registry.rs b/crates/trusted-server-core/src/integrations/registry.rs index 8b55493be..c7b2595aa 100644 --- a/crates/trusted-server-core/src/integrations/registry.rs +++ b/crates/trusted-server-core/src/integrations/registry.rs @@ -8,8 +8,8 @@ use fastly::http::Method; use fastly::{Request, Response}; use matchit::Router; +use crate::compat; use crate::constants::HEADER_X_TS_EC; -use crate::cookies::set_ec_cookie; use crate::edge_cookie::get_or_generate_ec_id; use crate::error::TrustedServerError; use crate::platform::RuntimeServices; @@ -688,7 +688,7 @@ impl IntegrationRegistry { // Cookie is intentionally not set when EC ID contains RFC 6265-illegal // characters (e.g. a crafted x-ts-ec header value). The response header // is still emitted; only cookie persistence is skipped. - set_ec_cookie(settings, response, ec_id.as_str()); + compat::set_fastly_ec_cookie(settings, response, ec_id.as_str()); } Err(ref err) => { log::warn!("Failed to generate EC ID for integration response: {err:?}"); diff --git a/crates/trusted-server-core/src/lib.rs b/crates/trusted-server-core/src/lib.rs index 44fa108dd..e1af33b72 100644 --- a/crates/trusted-server-core/src/lib.rs +++ b/crates/trusted-server-core/src/lib.rs @@ -35,6 +35,8 @@ pub mod auction; pub mod auction_config_types; pub mod auth; pub mod backend; +#[doc(hidden)] +pub mod compat; pub mod consent; pub mod consent_config; pub mod constants; @@ -62,3 +64,6 @@ pub mod streaming_processor; pub mod streaming_replacer; pub mod test_support; pub mod tsjs; + +#[cfg(test)] +mod migration_guards; diff --git a/crates/trusted-server-core/src/migration_guards.rs b/crates/trusted-server-core/src/migration_guards.rs new file mode 100644 index 000000000..f23fe6510 --- /dev/null +++ b/crates/trusted-server-core/src/migration_guards.rs @@ -0,0 +1,54 @@ +// Strips lines whose first non-whitespace token is `//`. +// +// Known limitations (both produce false positives, never false negatives): +// - String literals: `"fastly::Request"` in a test assertion would trigger a +// spurious failure even though it is not a real Fastly dependency. +// - Block comments: `/* fastly::Request */` is not stripped. A banned pattern +// inside a block comment causes a spurious failure; one hidden *outside* a +// block comment is still caught by the non-comment portions of the line. +// +// False positives are safe for a guard test — they cause a noisy failure that +// forces investigation rather than letting a real regression slip through +// silently. False negatives are not possible with the current banned-pattern +// list because none of the migrated files use block comments in practice. +fn strip_line_comments(source: &str) -> String { + source + .lines() + .filter(|line| { + let trimmed = line.trim_start(); + !trimmed.starts_with("//") + }) + .collect::>() + .join("\n") +} + +#[test] +fn migrated_utility_modules_do_not_depend_on_fastly_request_response_types() { + let sources = [ + ("auth.rs", include_str!("auth.rs")), + ("cookies.rs", include_str!("cookies.rs")), + ("http_util.rs", include_str!("http_util.rs")), + ( + "consent/extraction.rs", + include_str!("consent/extraction.rs"), + ), + ("consent/mod.rs", include_str!("consent/mod.rs")), + ]; + let banned_patterns = [ + "fastly::Request", + "fastly::Response", + "fastly::http::Method", + "fastly::http::StatusCode", + "fastly::mime::APPLICATION_JSON", + ]; + + for (path, source) in sources { + let uncommented = strip_line_comments(source); + for banned in banned_patterns { + assert!( + !uncommented.contains(banned), + "{path} should not reference `{banned}` after PR11 migration" + ); + } + } +} diff --git a/crates/trusted-server-core/src/publisher.rs b/crates/trusted-server-core/src/publisher.rs index 5bcef6941..12e6f0baf 100644 --- a/crates/trusted-server-core/src/publisher.rs +++ b/crates/trusted-server-core/src/publisher.rs @@ -18,9 +18,10 @@ use fastly::http::{header, StatusCode}; use fastly::{Body, Request, Response}; use crate::backend::BackendConfig; +use crate::compat; use crate::consent::{allows_ec_creation, build_consent_context, ConsentPipelineInput}; use crate::constants::{COOKIE_TS_EC, HEADER_X_COMPRESS_HINT, HEADER_X_TS_EC}; -use crate::cookies::{expire_ec_cookie, handle_request_cookies, set_ec_cookie}; +use crate::cookies::handle_request_cookies; use crate::edge_cookie::get_or_generate_ec_id; use crate::error::TrustedServerError; use crate::http_util::{serve_static_with_etag, RequestInfo}; @@ -129,12 +130,15 @@ pub fn handle_tsjs_dynamic( return Ok(Response::from_status(StatusCode::NOT_FOUND).with_body("Not Found")); } let filename = &path[PREFIX.len()..]; + let http_req = compat::from_fastly_headers_ref(req); if UNIFIED_FILENAMES.contains(&filename) { // Serve core + immediate modules (excludes deferred like prebid) let module_ids = integration_registry.js_module_ids_immediate(); let body = trusted_server_js::concatenate_modules(&module_ids); - let mut resp = serve_static_with_etag(&body, req, "application/javascript; charset=utf-8"); + let http_resp = + serve_static_with_etag(&body, &http_req, "application/javascript; charset=utf-8"); + let mut resp = compat::to_fastly_response(http_resp); resp.set_header(HEADER_X_COMPRESS_HINT, "on"); return Ok(resp); } @@ -146,8 +150,9 @@ pub fn handle_tsjs_dynamic( return Ok(Response::from_status(StatusCode::NOT_FOUND).with_body("Not Found")); } if let Some(content) = trusted_server_js::module_bundle(module_id) { - let mut resp = - serve_static_with_etag(content, req, "application/javascript; charset=utf-8"); + let http_resp = + serve_static_with_etag(content, &http_req, "application/javascript; charset=utf-8"); + let mut resp = compat::to_fastly_response(http_resp); resp.set_header(HEADER_X_COMPRESS_HINT, "on"); return Ok(resp); } @@ -452,8 +457,10 @@ pub fn handle_publisher_request( // Prebid.js requests are not intercepted here anymore. The HTML processor removes // publisher-supplied Prebid scripts; the unified TSJS bundle includes Prebid.js when enabled. + let http_req = compat::from_fastly_headers_ref(&req); + // Extract request host and scheme (uses Host header and TLS detection after edge sanitization) - let request_info = RequestInfo::from_request(&req, &services.client_info); + let request_info = RequestInfo::from_request(&http_req, &services.client_info); let request_host = &request_info.host; let request_scheme = &request_info.scheme; @@ -467,7 +474,7 @@ pub fn handle_publisher_request( ); // Parse cookies once for reuse by both consent extraction and EC ID logic. - let cookie_jar = handle_request_cookies(&req)?; + let cookie_jar = handle_request_cookies(&http_req)?; // Capture the current EC cookie value for revocation handling. // This must come from the cookie itself (not the x-ts-ec header) @@ -496,7 +503,7 @@ pub fn handle_publisher_request( }); let consent_context = build_consent_context(&ConsentPipelineInput { jar: cookie_jar.as_ref(), - req: &req, + req: &http_req, config: &settings.consent, geo: geo.as_ref(), ec_id: Some(ec_id.as_str()), @@ -697,14 +704,14 @@ fn apply_ec_headers( response.set_header(HEADER_X_TS_EC, ec_id); // Cookie persistence is skipped if the EC ID contains RFC 6265-illegal // characters. The header is still emitted when consent allows it. - set_ec_cookie(settings, response, ec_id); + compat::set_fastly_ec_cookie(settings, response, ec_id); } else if let Some(cookie_ec_id) = existing_ec_cookie { log::info!( "EC revoked for '{}': consent withdrawn (jurisdiction={})", cookie_ec_id, consent_context.jurisdiction, ); - expire_ec_cookie(settings, response); + compat::expire_fastly_ec_cookie(settings, response); if settings.consent.consent_store.is_some() { crate::consent::kv::delete_consent_from_kv(services.kv_store(), cookie_ec_id); } @@ -1188,7 +1195,8 @@ mod tests { req.set_header("x-ts-ec", "header_id"); req.set_header("cookie", "ts-ec=cookie_id; other=value"); - let cookie_jar = handle_request_cookies(&req).expect("should parse cookies"); + let http_req = compat::from_fastly_headers_ref(&req); + let cookie_jar = handle_request_cookies(&http_req).expect("should parse cookies"); let existing_ec_cookie = cookie_jar .as_ref() .and_then(|jar| jar.get(COOKIE_TS_EC)) diff --git a/crates/trusted-server-core/src/request_signing/endpoints.rs b/crates/trusted-server-core/src/request_signing/endpoints.rs index cba71ab48..988d747fe 100644 --- a/crates/trusted-server-core/src/request_signing/endpoints.rs +++ b/crates/trusted-server-core/src/request_signing/endpoints.rs @@ -48,7 +48,7 @@ pub fn handle_trusted_server_discovery( )?; Ok(Response::from_status(200) - .with_content_type(fastly::mime::APPLICATION_JSON) + .with_content_type(mime::APPLICATION_JSON) .with_body(json)) } @@ -133,7 +133,7 @@ pub fn handle_verify_signature( })?; Ok(Response::from_status(200) - .with_content_type(fastly::mime::APPLICATION_JSON) + .with_content_type(mime::APPLICATION_JSON) .with_body(response_json)) } @@ -275,7 +275,7 @@ pub fn handle_rotate_key( })?; Ok(Response::from_status(200) - .with_content_type(fastly::mime::APPLICATION_JSON) + .with_content_type(mime::APPLICATION_JSON) .with_body(response_json)) } Err(e) => { @@ -297,7 +297,7 @@ pub fn handle_rotate_key( })?; Ok(Response::from_status(status) - .with_content_type(fastly::mime::APPLICATION_JSON) + .with_content_type(mime::APPLICATION_JSON) .with_body(response_json)) } } @@ -398,7 +398,7 @@ pub fn handle_deactivate_key( })?; Ok(Response::from_status(200) - .with_content_type(fastly::mime::APPLICATION_JSON) + .with_content_type(mime::APPLICATION_JSON) .with_body(response_json)) } Err(e) => { @@ -423,7 +423,7 @@ pub fn handle_deactivate_key( })?; Ok(Response::from_status(status) - .with_content_type(fastly::mime::APPLICATION_JSON) + .with_content_type(mime::APPLICATION_JSON) .with_body(response_json)) } } @@ -490,7 +490,7 @@ mod tests { assert_eq!(resp.get_status(), StatusCode::OK); assert_eq!( resp.get_content_type(), - Some(fastly::mime::APPLICATION_JSON), + Some(mime::APPLICATION_JSON), "should return application/json content type" ); @@ -530,7 +530,7 @@ mod tests { assert_eq!(resp.get_status(), StatusCode::OK); assert_eq!( resp.get_content_type(), - Some(fastly::mime::APPLICATION_JSON), + Some(mime::APPLICATION_JSON), "should return application/json content type" );