From 81dd31d5aab45a2bda42ff068c4bc7efe18e081b Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Tue, 7 Apr 2026 15:27:42 +0530 Subject: [PATCH 1/2] Migrate utility layer to http::Request/Response and add compat adapter Migrates http_util, auth, cookies, and edge_cookie modules from fastly::Request/Response to http::Request/http::Response as part of the EdgeZero phase 2 platform migration. Adds compat.rs as a temporary bridge module that lets handler and integration layer code (which still holds fastly::Request) call into the migrated utility functions without converting the entire call stack in this PR. All bridge functions are marked TODO(PR15) for removal once the handler layer is migrated. Extension types TlsProtocol, TlsCipher, and ClientIpExt carry Fastly-specific TLS and client IP data through http::Request extensions so that detect_request_scheme and generate_ec_id can access them without a Fastly SDK import in the utility layer. Closes #492 --- .../trusted-server-adapter-fastly/src/main.rs | 7 +- .../src/auction/endpoints.rs | 12 +- .../src/auction/formats.rs | 10 +- crates/trusted-server-core/src/auth.rs | 97 +-- crates/trusted-server-core/src/compat.rs | 640 ++++++++++++++++++ crates/trusted-server-core/src/cookies.rs | 91 ++- crates/trusted-server-core/src/edge_cookie.rs | 59 +- crates/trusted-server-core/src/http_util.rs | 319 +++++---- .../src/integrations/lockr.rs | 7 +- .../src/integrations/permutive.rs | 4 +- .../src/integrations/prebid.rs | 9 +- .../src/integrations/registry.rs | 7 +- .../src/integrations/testlight.rs | 4 +- crates/trusted-server-core/src/lib.rs | 1 + crates/trusted-server-core/src/proxy.rs | 6 +- crates/trusted-server-core/src/publisher.rs | 32 +- 16 files changed, 1033 insertions(+), 272 deletions(-) create mode 100644 crates/trusted-server-core/src/compat.rs diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index 72cae33ea..850faa657 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -5,14 +5,13 @@ use log_fastly::Logger; 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::{ @@ -97,7 +96,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_forwarded_headers_fastly(&mut req); // Look up geo info via the platform abstraction using the client IP // already captured in RuntimeServices at the entry point. @@ -112,7 +111,7 @@ 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) { + match compat::enforce_basic_auth_fastly(settings, &req) { Ok(Some(mut response)) => { finalize_response(settings, geo_info.as_ref(), &mut response); return Ok(response); diff --git a/crates/trusted-server-core/src/auction/endpoints.rs b/crates/trusted-server-core/src/auction/endpoints.rs index 018b2b383..324c3f7de 100644 --- a/crates/trusted-server-core/src/auction/endpoints.rs +++ b/crates/trusted-server-core/src/auction/endpoints.rs @@ -4,9 +4,8 @@ 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; use crate::error::TrustedServerError; use crate::geo::GeoInfo; use crate::platform::RuntimeServices; @@ -49,13 +48,14 @@ pub async fn handle_auction( // 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, &req).change_context(TrustedServerError::Auction { + let ec_id = compat::get_or_generate_ec_id_fastly(settings, &req).change_context( + TrustedServerError::Auction { message: "Failed to generate EC ID".to_string(), - })?; + }, + )?; // Extract consent from request cookies, headers, and geo. - let cookie_jar = handle_request_cookies(&req)?; + let cookie_jar = compat::handle_request_cookies_fastly(&req)?; #[allow(deprecated)] let geo = GeoInfo::from_request(&req); let consent_context = consent::build_consent_context(&consent::ConsentPipelineInput { diff --git a/crates/trusted-server-core/src/auction/formats.rs b/crates/trusted-server-core/src/auction/formats.rs index 1f557a17e..af342c9d8 100644 --- a/crates/trusted-server-core/src/auction/formats.rs +++ b/crates/trusted-server-core/src/auction/formats.rs @@ -13,10 +13,10 @@ use std::collections::HashMap; use uuid::Uuid; use crate::auction::context::ContextValue; +use crate::compat; use crate::consent::ConsentContext; use crate::constants::{HEADER_X_TS_EC, HEADER_X_TS_EC_FRESH}; use crate::creative; -use crate::edge_cookie::generate_ec_id; use crate::error::TrustedServerError; use crate::geo::GeoInfo; use crate::openrtb::{to_openrtb_i32, OpenRtbBid, OpenRtbResponse, ResponseExt, SeatBid, ToExt}; @@ -88,9 +88,11 @@ pub fn convert_tsjs_to_auction_request( ec_id: &str, ) -> Result> { let ec_id = ec_id.to_owned(); - let fresh_id = generate_ec_id(settings, req).change_context(TrustedServerError::Auction { - message: "Failed to generate fresh EC ID".to_string(), - })?; + let fresh_id = compat::generate_ec_id_fastly(settings, req).change_context( + TrustedServerError::Auction { + message: "Failed to generate fresh EC ID".to_string(), + }, + )?; // Convert ad units to slots let mut slots = Vec::new(); diff --git a/crates/trusted-server-core/src/auth.rs b/crates/trusted-server-core/src/auth.rs index 547784dfe..68e292eda 100644 --- a/crates/trusted-server-core/src/auth.rs +++ b/crates/trusted-server-core/src/auth.rs @@ -1,7 +1,7 @@ use base64::{engine::general_purpose::STANDARD, Engine as _}; +use edgezero_core::body::Body; use error_stack::Report; -use fastly::http::{header, StatusCode}; -use fastly::{Request, Response}; +use http::{header, StatusCode}; use sha2::{Digest as _, Sha256}; use subtle::ConstantTimeEq as _; @@ -27,9 +27,10 @@ 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: &http::Request, +) -> Result>, Report> { + let path = req.uri().path(); + let Some(handler) = settings.handler_for_path(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: {}", path); Ok(Some(unauthorized_response())) } } -fn extract_credentials(req: &Request) -> Option<(String, String)> { +fn extract_credentials(req: &http::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,43 @@ 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() -> http::Response { + http::Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header(header::WWW_AUTHENTICATE, BASIC_AUTH_REALM) + .header(header::CONTENT_TYPE, "text/plain; charset=utf-8") + .body(Body::from("Unauthorized")) + .expect("should build unauthorized response") } #[cfg(test)] mod tests { use super::*; use base64::engine::general_purpose::STANDARD; - use fastly::http::{header, Method}; use crate::test_support::tests::{crate_test_settings_str, create_test_settings}; + fn make_request(method: &str, uri: &str) -> http::Request { + http::Request::builder() + .method(method) + .uri(uri) + .body(Body::empty()) + .expect("should build test request") + } + + fn make_request_with_auth(method: &str, uri: &str, token: &str) -> http::Request { + http::Request::builder() + .method(method) + .uri(uri) + .header(header::AUTHORIZATION, format!("Basic {token}")) + .body(Body::empty()) + .expect("should build test request with auth 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 = make_request("GET", "https://example.com/open"); assert!(enforce_basic_auth(&settings, &req) .expect("should evaluate auth") @@ -112,14 +132,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 = make_request("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 +148,8 @@ 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 token = STANDARD.encode("user:pass"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + let req = make_request_with_auth("GET", "https://example.com/secure/data", &token); assert!(enforce_basic_auth(&settings, &req) .expect("should evaluate auth") @@ -139,29 +159,27 @@ 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 token = STANDARD.encode("wrong:wrong"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + let req = make_request_with_auth("GET", "https://example.com/secure/data", &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 token = STANDARD.encode("wrong-user:pass"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + let req = make_request_with_auth("GET", "https://example.com/secure/data", &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 +188,14 @@ 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 token = STANDARD.encode("user:wrong-pass"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + let req = make_request_with_auth("GET", "https://example.com/secure/data", &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 +204,17 @@ 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 req = http::Request::builder() + .method("GET") + .uri("https://example.com/secure") + .header(header::AUTHORIZATION, "Bearer token") + .body(Body::empty()) + .expect("should build test request"); 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 +231,8 @@ 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 token = STANDARD.encode("admin:admin-pass"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + let req = make_request_with_auth("POST", "https://example.com/admin/keys/rotate", &token); assert!( enforce_basic_auth(&settings, &req) @@ -225,24 +245,23 @@ 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 token = STANDARD.encode("admin:wrong"); - req.set_header(header::AUTHORIZATION, format!("Basic {token}")); + let req = make_request_with_auth("POST", "https://example.com/admin/keys/rotate", &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 = make_request("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..63e5ae7bd --- /dev/null +++ b/crates/trusted-server-core/src/compat.rs @@ -0,0 +1,640 @@ +//! Compatibility adapter bridging `fastly::Request/Response` with `http::Request/Response`. +//! +//! This module provides type conversion functions and request extension types used during the +//! Phase 2 of the platform migration. All items here are temporary bridges scheduled for removal +//! once the full type migration is complete. +//! +//! ## Usage pattern +//! +//! Handler-layer functions that still accept `fastly::Request` call these helpers at each +//! utility-function boundary: +//! +//! ```ignore +//! // read-only: borrow-based — no body copy +//! let http_req = compat::from_fastly_request_ref(&req); +//! let info = http_util::RequestInfo::from_request(&http_req); +//! +//! // mutable: build an http outgoing request, then convert back +//! let mut http_out = http::Request::builder().method(...).uri(...).body(Body::empty())...; +//! cookies::forward_cookie_header(&http_req, &mut http_out, true); +//! let fastly_out = compat::to_fastly_request(http_out); +//! ``` +//! +//! # TODO(PR15) +//! +//! Remove this entire module after handler-layer type migration is complete. + +use std::net::IpAddr; + +use edgezero_core::body::Body; +use http::{HeaderName, HeaderValue, StatusCode}; + +// ── Request extension types ──────────────────────────────────────────────── + +/// TLS protocol string detected by the Fastly SDK. +/// +/// Stored as a [`http::Request`] extension by [`from_fastly_request_ref`] so that +/// `http_util::detect_request_scheme` can check it without Fastly SDK access. +/// +/// # TODO(PR15): Remove once handler-layer migration is complete. +#[derive(Debug, Clone)] +pub struct TlsProtocol(pub Option<&'static str>); + +/// TLS cipher string detected by the Fastly SDK. +/// +/// Stored as a [`http::Request`] extension by [`from_fastly_request_ref`] so that +/// `http_util::detect_request_scheme` can check it without Fastly SDK access. +/// +/// # TODO(PR15): Remove once handler-layer migration is complete. +#[derive(Debug, Clone)] +pub struct TlsCipher(pub Option<&'static str>); + +/// Client IP address captured from the Fastly SDK. +/// +/// Stored as a [`http::Request`] extension by [`from_fastly_request_ref`] so that +/// `edge_cookie::generate_ec_id` can read the client IP without Fastly SDK access. +/// +/// # TODO(PR15): Remove once handler-layer migration is complete. +#[derive(Debug, Clone)] +pub struct ClientIpExt(pub Option); + +// ── Request conversions ──────────────────────────────────────────────────── + +/// Create an `http::Request` from a `&fastly::Request` reference. +/// +/// Copies all headers and the URI from the Fastly request. The body is **not** copied — +/// the returned request carries an empty body. Fastly-specific TLS and client IP +/// information is preserved in the request's extensions. +/// +/// Prefer this over [`from_fastly_request`] for utility calls that only read +/// headers or the request path, so that the original `fastly::Request` body is +/// not consumed. +/// +/// # Panics +/// +/// Panics if the Fastly request URL cannot be parsed as an `http::Uri`. +/// +/// # TODO(PR15): Remove after handler-layer migration is complete. +pub fn from_fastly_request_ref(req: &fastly::Request) -> http::Request { + let uri: http::Uri = req + .get_url_str() + .parse() + .expect("should parse fastly request URL as URI"); + + let mut builder = http::Request::builder() + .method(req.get_method().clone()) + .uri(uri); + + for (name, value) in req.get_headers() { + builder = builder.header(name.clone(), value.clone()); + } + + let mut http_req = builder + .body(Body::empty()) + .expect("should build http request from fastly request reference"); + + http_req + .extensions_mut() + .insert(TlsProtocol(req.get_tls_protocol())); + http_req + .extensions_mut() + .insert(TlsCipher(req.get_tls_cipher_openssl_name())); + http_req + .extensions_mut() + .insert(ClientIpExt(req.get_client_ip_addr())); + + http_req +} + +/// Consume a `fastly::Request` and produce an `http::Request`. +/// +/// Moves the request body out of the Fastly request. Use this when the utility layer +/// needs to read or process the request body. Fastly-specific TLS and client IP +/// information is preserved in the request's extensions. +/// +/// # Panics +/// +/// Panics if the Fastly request URL cannot be parsed as an `http::Uri`. +/// +/// # TODO(PR15): Remove after handler-layer migration is complete. +pub fn from_fastly_request(mut req: fastly::Request) -> http::Request { + let uri: http::Uri = req + .get_url_str() + .parse() + .expect("should parse fastly request URL as URI"); + + let tls_protocol = req.get_tls_protocol(); + let tls_cipher = req.get_tls_cipher_openssl_name(); + let client_ip = req.get_client_ip_addr(); + + let mut builder = http::Request::builder() + .method(req.get_method().clone()) + .uri(uri); + + for (name, value) in req.get_headers() { + builder = builder.header(name.clone(), value.clone()); + } + + let body = Body::from_bytes(req.take_body_bytes()); + + let mut http_req = builder + .body(body) + .expect("should build http request from fastly request"); + + http_req.extensions_mut().insert(TlsProtocol(tls_protocol)); + http_req.extensions_mut().insert(TlsCipher(tls_cipher)); + http_req.extensions_mut().insert(ClientIpExt(client_ip)); + + http_req +} + +/// Convert an `http::Request` back to a `fastly::Request`. +/// +/// Used at integration layer boundaries where the utility layer produces an +/// `http::Request` (e.g. after header manipulation) but the downstream +/// operation still requires a `fastly::Request` for sending via the Fastly SDK. +/// +/// Body streaming is not supported — the body is materialised into bytes before +/// conversion. Avoid this function for large bodies until streaming support is added. +/// +/// # TODO(PR15): Remove after handler-layer migration is complete. +pub fn to_fastly_request(req: http::Request) -> fastly::Request { + let (parts, body) = req.into_parts(); + + let mut fastly_req = fastly::Request::new(parts.method, parts.uri.to_string()); + + for (name, value) in &parts.headers { + fastly_req.set_header(name, value); + } + + let body_bytes = body.into_bytes(); + if !body_bytes.is_empty() { + fastly_req.set_body_octet_stream(&body_bytes); + } + + fastly_req +} + +// ── Response conversions ─────────────────────────────────────────────────── + +/// Convert a `fastly::Response` to an `http::Response`. +/// +/// Body streaming is not supported — the body is materialised into bytes. +/// +/// # Panics +/// +/// Panics if the `http::Response` builder fails (unreachable in practice). +/// +/// # TODO(PR15): Remove after handler-layer migration is complete. +pub fn from_fastly_response(mut res: fastly::Response) -> http::Response { + let mut builder = http::Response::builder().status(res.get_status()); + + for (name, value) in res.get_headers() { + builder = builder.header(name.clone(), value.clone()); + } + + let body = Body::from_bytes(res.take_body_bytes()); + + builder + .body(body) + .expect("should build http response from fastly response") +} + +/// Convert an `http::Response` to a `fastly::Response`. +/// +/// Body streaming is not supported — the body is materialised into bytes. +/// +/// # TODO(PR15): Remove after handler-layer migration is complete. +pub fn to_fastly_response(res: http::Response) -> fastly::Response { + let (parts, body) = res.into_parts(); + + let mut fastly_res = fastly::Response::from_status(parts.status); + + for (name, value) in &parts.headers { + fastly_res.set_header(name, value); + } + + let body_bytes = body.into_bytes(); + if !body_bytes.is_empty() { + fastly_res.set_body_octet_stream(&body_bytes); + } + + fastly_res +} + +// ── Response builder helpers ─────────────────────────────────────────────── + +/// Build an `http::Response` with a given status and no body. +/// +/// # Panics +/// +/// Panics if the `http::Response` builder fails (unreachable in practice). +/// +/// # TODO(PR15): Remove — callers should use `http::Response::builder()` directly. +#[must_use] +pub fn response_from_status(status: StatusCode) -> http::Response { + http::Response::builder() + .status(status) + .body(Body::empty()) + .expect("should build response from status") +} + +/// Append a header to an `http::Response`, returning the response. +/// +/// Unlike [`http::Response::headers_mut().append()`], this is chainable. +/// +/// # TODO(PR15): Remove — callers should use `response.headers_mut().append()` directly. +pub fn append_header_to_response( + mut res: http::Response, + name: HeaderName, + value: HeaderValue, +) -> http::Response { + res.headers_mut().append(name, value); + res +} + +// ── Fastly-boundary bridge functions ────────────────────────────────────── +// +// These are handler/integration-layer shims that let code still holding +// `fastly::Request` / `fastly::Response` call into the migrated utility +// functions without converting the entire call stack in PR 11. They are +// removed in PR 15 once the handler and integration layers are fully migrated. + +/// Apply `http_util::sanitize_forwarded_headers` to a `fastly::Request`. +/// +/// Removes client-spoofable forwarded headers in place. +/// +/// # TODO(PR15): Remove once the handler layer migrates to `http` types. +pub fn sanitize_forwarded_headers_fastly(req: &mut fastly::Request) { + use crate::http_util::SPOOFABLE_FORWARDED_HEADERS; + for header in SPOOFABLE_FORWARDED_HEADERS { + if req.get_header(*header).is_some() { + log::debug!("Stripped spoofable header: {}", header); + req.remove_header(*header); + } + } +} + +/// Apply `auth::enforce_basic_auth` with a `fastly::Request`, returning a `fastly::Response`. +/// +/// Converts the request to `http::Request` and the returned response to +/// `fastly::Response` for the adapter entry point. +/// +/// # Errors +/// +/// Returns an error when handler configuration is invalid. +/// +/// # TODO(PR15): Remove once the adapter entry point migrates to `http` types. +pub fn enforce_basic_auth_fastly( + settings: &crate::settings::Settings, + req: &fastly::Request, +) -> Result, error_stack::Report> { + use crate::auth; + let http_req = from_fastly_request_ref(req); + auth::enforce_basic_auth(settings, &http_req).map(|opt| opt.map(to_fastly_response)) +} + +/// Apply `http_util::serve_static_with_etag` with a `fastly::Request`, returning a +/// `fastly::Response`. +/// +/// # TODO(PR15): Remove once the handler layer migrates to `http` types. +pub fn serve_static_with_etag_fastly( + body: &str, + req: &fastly::Request, + content_type: &str, +) -> fastly::Response { + use crate::http_util; + let http_req = from_fastly_request_ref(req); + let http_resp = http_util::serve_static_with_etag(body, &http_req, content_type); + to_fastly_response(http_resp) +} + +/// Apply `cookies::set_ec_cookie` to a `fastly::Response`. +/// +/// Creates a temporary `http::Response` to collect the Set-Cookie header, +/// then appends it to the Fastly response. +/// +/// # Panics +/// +/// Panics if the temporary `http::Response` builder fails (unreachable in practice). +/// +/// # TODO(PR15): Remove once publisher/registry migrate to `http` types. +pub fn set_ec_cookie_fastly( + settings: &crate::settings::Settings, + response: &mut fastly::Response, + ec_id: &str, +) { + use crate::cookies; + let mut temp = http::Response::builder() + .status(200u16) + .body(Body::empty()) + .expect("should build temp response for cookie collection"); + cookies::set_ec_cookie(settings, &mut temp, ec_id); + for value in temp.headers().get_all(http::header::SET_COOKIE) { + response.append_header(http::header::SET_COOKIE, value); + } +} + +/// Apply `cookies::expire_ec_cookie` to a `fastly::Response`. +/// +/// # Panics +/// +/// Panics if the temporary `http::Response` builder fails (unreachable in practice). +/// +/// # TODO(PR15): Remove once publisher/registry migrate to `http` types. +pub fn expire_ec_cookie_fastly( + settings: &crate::settings::Settings, + response: &mut fastly::Response, +) { + use crate::cookies; + let mut temp = http::Response::builder() + .status(200u16) + .body(Body::empty()) + .expect("should build temp response for cookie collection"); + cookies::expire_ec_cookie(settings, &mut temp); + for value in temp.headers().get_all(http::header::SET_COOKIE) { + response.append_header(http::header::SET_COOKIE, value); + } +} + +/// Apply `cookies::forward_cookie_header` with `fastly::Request` types. +/// +/// Converts `from` to `http::Request`, runs the migrated utility, then +/// applies any Cookie header changes back to `to`. +/// +/// # Panics +/// +/// Panics if the temporary `http::Request` builder fails (unreachable in practice). +/// +/// # TODO(PR15): Remove once integration layer migrates to `http` types. +pub fn forward_cookie_header_fastly( + from: &fastly::Request, + to: &mut fastly::Request, + strip_consent: bool, +) { + use crate::cookies; + let http_from = from_fastly_request_ref(from); + let mut http_to = http::Request::builder() + .method("GET") + .uri("/") + .body(Body::empty()) + .expect("should build temp request for cookie forwarding"); + cookies::forward_cookie_header(&http_from, &mut http_to, strip_consent); + match http_to.headers().get(http::header::COOKIE) { + Some(cookie) => to.set_header(http::header::COOKIE, cookie), + None => { + to.remove_header(http::header::COOKIE); + } + } +} + +/// Apply `http_util::copy_custom_headers` with `fastly::Request` types. +/// +/// Converts `from` to `http::Request`, runs the migrated utility, then +/// copies all resulting X-* headers to `to`. +/// +/// # Panics +/// +/// Panics if the temporary `http::Request` builder fails (unreachable in practice). +/// +/// # TODO(PR15): Remove once integration layer migrates to `http` types. +pub fn copy_custom_headers_fastly(from: &fastly::Request, to: &mut fastly::Request) { + use crate::http_util; + let http_from = from_fastly_request_ref(from); + let mut http_to = http::Request::builder() + .method("GET") + .uri("/") + .body(Body::empty()) + .expect("should build temp request for custom header copying"); + http_util::copy_custom_headers(&http_from, &mut http_to); + for (name, value) in http_to.headers() { + to.set_header(name, value); + } +} + +/// Apply `http_util::RequestInfo::from_request` with a `fastly::Request`. +/// +/// # TODO(PR15): Remove once the handler layer migrates to `http` types. +pub fn request_info_from_fastly(req: &fastly::Request) -> crate::http_util::RequestInfo { + use crate::http_util::RequestInfo; + let http_req = from_fastly_request_ref(req); + RequestInfo::from_request(&http_req) +} + +/// Apply `edge_cookie::get_ec_id` with a `fastly::Request`. +/// +/// # Errors +/// +/// Returns an error if cookie parsing fails. +/// +/// # TODO(PR15): Remove once the handler layer migrates to `http` types. +pub fn get_ec_id_fastly( + req: &fastly::Request, +) -> Result, error_stack::Report> { + use crate::edge_cookie; + let http_req = from_fastly_request_ref(req); + edge_cookie::get_ec_id(&http_req) +} + +/// Apply `edge_cookie::get_or_generate_ec_id` with a `fastly::Request`. +/// +/// # Errors +/// +/// Returns an error if EC ID generation fails. +/// +/// # TODO(PR15): Remove once the handler layer migrates to `http` types. +pub fn get_or_generate_ec_id_fastly( + settings: &crate::settings::Settings, + req: &fastly::Request, +) -> Result> { + use crate::edge_cookie; + let http_req = from_fastly_request_ref(req); + edge_cookie::get_or_generate_ec_id(settings, &http_req) +} + +/// Apply `edge_cookie::generate_ec_id` with a `fastly::Request`. +/// +/// # Errors +/// +/// Returns an error if EC ID generation fails. +/// +/// # TODO(PR15): Remove once the handler layer migrates to `http` types. +pub fn generate_ec_id_fastly( + settings: &crate::settings::Settings, + req: &fastly::Request, +) -> Result> { + use crate::edge_cookie; + let http_req = from_fastly_request_ref(req); + edge_cookie::generate_ec_id(settings, &http_req) +} + +/// Apply `cookies::handle_request_cookies` with a `fastly::Request`. +/// +/// # Errors +/// +/// Returns an error if the Cookie header contains invalid UTF-8. +/// +/// # TODO(PR15): Remove once the handler layer migrates to `http` types. +pub fn handle_request_cookies_fastly( + req: &fastly::Request, +) -> Result, error_stack::Report> { + use crate::cookies; + let http_req = from_fastly_request_ref(req); + cookies::handle_request_cookies(&http_req) +} + +#[cfg(test)] +mod tests { + use super::*; + use http::header; + + fn make_fastly_req_with_headers() -> fastly::Request { + let mut req = + fastly::Request::new(fastly::http::Method::GET, "https://example.com/path?q=1"); + req.set_header("x-custom", "value"); + req.set_header(header::HOST, "example.com"); + req + } + + #[test] + fn from_fastly_request_ref_copies_headers_and_uri() { + let fastly_req = make_fastly_req_with_headers(); + let http_req = from_fastly_request_ref(&fastly_req); + + assert_eq!( + http_req + .headers() + .get("x-custom") + .map(http::HeaderValue::as_bytes), + Some(b"value".as_ref()), + "should copy x-custom header" + ); + assert_eq!( + http_req.uri().path(), + "/path", + "should preserve request path" + ); + assert_eq!( + http_req.uri().query(), + Some("q=1"), + "should preserve query string" + ); + } + + #[test] + fn from_fastly_request_ref_has_empty_body() { + let fastly_req = make_fastly_req_with_headers(); + let http_req = from_fastly_request_ref(&fastly_req); + + assert!( + http_req.body().as_bytes().is_empty(), + "should have empty body when using reference conversion" + ); + } + + #[test] + fn from_fastly_request_ref_stores_client_ip_extension() { + let fastly_req = make_fastly_req_with_headers(); + let http_req = from_fastly_request_ref(&fastly_req); + + assert!( + http_req.extensions().get::().is_some(), + "should store ClientIpExt extension" + ); + } + + #[test] + fn from_fastly_request_ref_stores_tls_extensions() { + let fastly_req = make_fastly_req_with_headers(); + let http_req = from_fastly_request_ref(&fastly_req); + + assert!( + http_req.extensions().get::().is_some(), + "should store TlsProtocol extension" + ); + assert!( + http_req.extensions().get::().is_some(), + "should store TlsCipher extension" + ); + } + + #[test] + fn from_fastly_request_copies_body() { + let mut fastly_req = fastly::Request::post("https://example.com/api"); + fastly_req.set_body_octet_stream(b"hello body"); + + let http_req = from_fastly_request(fastly_req); + + assert_eq!( + http_req.body().as_bytes(), + b"hello body", + "should copy request body bytes" + ); + } + + #[test] + fn to_fastly_request_preserves_method_uri_headers() { + let http_req = http::Request::builder() + .method("POST") + .uri("https://api.example.com/submit") + .header("x-req-id", "abc-123") + .body(Body::empty()) + .expect("should build http request"); + + let fastly_req = to_fastly_request(http_req); + + assert_eq!( + fastly_req.get_method_str(), + "POST", + "should preserve HTTP method" + ); + assert!( + fastly_req.get_url_str().contains("api.example.com"), + "should preserve URL" + ); + assert!( + fastly_req.get_header("x-req-id").is_some(), + "should preserve request headers" + ); + } + + #[test] + fn response_roundtrip_preserves_status_and_headers() { + let mut fastly_res = fastly::Response::from_status(202); + fastly_res.set_header("x-resp-id", "xyz"); + + let http_res = from_fastly_response(fastly_res); + assert_eq!( + http_res.status().as_u16(), + 202, + "should preserve status code" + ); + assert_eq!( + http_res + .headers() + .get("x-resp-id") + .map(http::HeaderValue::as_bytes), + Some(b"xyz".as_ref()), + "should preserve response headers" + ); + + let fastly_res2 = to_fastly_response(http_res); + assert_eq!( + fastly_res2.get_status().as_u16(), + 202, + "should round-trip status code" + ); + assert!( + fastly_res2.get_header("x-resp-id").is_some(), + "should round-trip response headers" + ); + } + + #[test] + fn response_from_status_has_correct_status_and_empty_body() { + let res = response_from_status(StatusCode::NOT_FOUND); + + assert_eq!(res.status().as_u16(), 404, "should have 404 status"); + assert!(res.body().as_bytes().is_empty(), "should have empty body"); + } +} diff --git a/crates/trusted-server-core/src/cookies.rs b/crates/trusted-server-core/src/cookies.rs index 963bf1256..7fba2c456 100644 --- a/crates/trusted-server-core/src/cookies.rs +++ b/crates/trusted-server-core/src/cookies.rs @@ -6,9 +6,9 @@ use std::borrow::Cow; use cookie::{Cookie, CookieJar}; +use edgezero_core::body::Body; use error_stack::{Report, ResultExt}; -use fastly::http::header; -use fastly::Request; +use http::header; use crate::constants::{ COOKIE_EUCONSENT_V2, COOKIE_GPP, COOKIE_GPP_SID, COOKIE_TS_EC, COOKIE_US_PRIVACY, @@ -97,9 +97,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: &http::Request, ) -> Result, Report> { - match req.get_header(header::COOKIE) { + match req.headers().get(header::COOKIE) { Some(header_value) => { let header_value_str = header_value @@ -146,13 +146,18 @@ pub fn strip_cookies(cookie_header: &str, cookie_names: &[&str]) -> String { /// 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 { +pub fn forward_cookie_header( + from: &http::Request, + to: &mut http::Request, + strip_consent: bool, +) { + let Some(cookie_value) = from.headers().get(header::COOKIE) else { return; }; if !strip_consent { - to.set_header(header::COOKIE, cookie_value); + to.headers_mut() + .insert(header::COOKIE, cookie_value.clone()); return; } @@ -160,12 +165,15 @@ pub fn forward_cookie_header(from: &Request, to: &mut Request, strip_consent: bo Ok(s) => { let stripped = strip_cookies(s, CONSENT_COOKIE_NAMES); if !stripped.is_empty() { - to.set_header(header::COOKIE, &stripped); + if let Ok(val) = http::HeaderValue::from_str(&stripped) { + to.headers_mut().insert(header::COOKIE, val); + } } } Err(_) => { // Non-UTF-8 Cookie header — forward as-is - to.set_header(header::COOKIE, cookie_value); + to.headers_mut() + .insert(header::COOKIE, cookie_value.clone()); } } } @@ -246,7 +254,7 @@ 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) { +pub fn set_ec_cookie(settings: &Settings, response: &mut http::Response, ec_id: &str) { if !is_safe_cookie_value(ec_id) { log::warn!( "Rejecting EC ID for Set-Cookie: value of {} bytes contains characters illegal in a cookie value", @@ -254,16 +262,21 @@ pub fn set_ec_cookie(settings: &Settings, response: &mut fastly::Response, ec_id ); return; } - response.append_header(header::SET_COOKIE, create_ec_cookie(settings, ec_id)); + let cookie_str = create_ec_cookie(settings, ec_id); + if let Ok(val) = http::HeaderValue::from_str(&cookie_str) { + response.headers_mut().append(header::SET_COOKIE, val); + } } /// 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); +pub fn expire_ec_cookie(settings: &Settings, response: &mut http::Response) { + let cookie = format!("{}=; {}", COOKIE_TS_EC, ec_cookie_attributes(settings, 0)); + if let Ok(val) = http::HeaderValue::from_str(&cookie) { + response.headers_mut().append(header::SET_COOKIE, val); + } } #[cfg(test)] @@ -272,6 +285,22 @@ mod tests { use super::*; + fn make_request_with_cookie(cookie_value: &str) -> http::Request { + http::Request::builder() + .method("GET") + .uri("http://example.com") + .header(header::COOKIE, cookie_value) + .body(Body::empty()) + .expect("should build test request with cookie") + } + + fn make_empty_response() -> http::Response { + http::Response::builder() + .status(200) + .body(Body::empty()) + .expect("should build empty response") + } + #[test] fn test_parse_cookies_to_jar() { let header_value = "c1=v1; c2=v2"; @@ -309,7 +338,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 = make_request_with_cookie("c1=v1;c2=v2"); let jar = handle_request_cookies(&req) .expect("should parse cookies") .expect("should have cookie jar"); @@ -321,7 +350,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 = make_request_with_cookie(""); let jar = handle_request_cookies(&req) .expect("should parse cookies") .expect("should have cookie jar"); @@ -331,7 +360,11 @@ mod tests { #[test] fn test_handle_request_cookies_no_cookie_header() { - let req: Request = Request::get("https://example.com"); + let req = http::Request::builder() + .method("GET") + .uri("https://example.com") + .body(Body::empty()) + .expect("should build request without cookie"); let jar = handle_request_cookies(&req).expect("should handle missing cookie header"); assert!(jar.is_none()); @@ -339,7 +372,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 = make_request_with_cookie("invalid"); let jar = handle_request_cookies(&req) .expect("should parse cookies") .expect("should have cookie jar"); @@ -350,11 +383,12 @@ mod tests { #[test] fn test_set_ec_cookie() { let settings = create_test_settings(); - let mut response = fastly::Response::new(); + let mut response = make_empty_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 +437,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 = make_empty_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 +449,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 = make_empty_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,11 +461,11 @@ mod tests { #[test] fn test_set_ec_cookie_rejects_space() { let settings = create_test_settings(); - let mut response = fastly::Response::new(); + let mut response = make_empty_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" ); } @@ -481,12 +515,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 = make_empty_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() diff --git a/crates/trusted-server-core/src/edge_cookie.rs b/crates/trusted-server-core/src/edge_cookie.rs index 063c3fddc..57adc2cef 100644 --- a/crates/trusted-server-core/src/edge_cookie.rs +++ b/crates/trusted-server-core/src/edge_cookie.rs @@ -5,12 +5,13 @@ use std::net::IpAddr; +use edgezero_core::body::Body; use error_stack::{Report, ResultExt}; -use fastly::Request; use hmac::{Hmac, Mac}; use rand::Rng; use sha2::Sha256; +use crate::compat::ClientIpExt; 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; @@ -62,17 +63,23 @@ fn generate_random_suffix(length: usize) -> String { /// the client IP address, then appends a random suffix for additional /// uniqueness. The resulting format is `{64hex}.{6alnum}`. /// +/// The client IP is read from the [`ClientIpExt`] request extension, which is +/// populated by [`crate::compat::from_fastly_request_ref`] from the Fastly SDK. +/// Falls back to `"unknown"` when the extension is absent (e.g. in tests). +/// /// # Errors /// /// - [`TrustedServerError::Ec`] if HMAC generation fails pub fn generate_ec_id( settings: &Settings, - req: &Request, + req: &http::Request, ) -> Result> { - // Fallback to "unknown" when client IP is unavailable (e.g., local testing). - // All such requests share the same HMAC base; the random suffix provides uniqueness. + // Read client IP from the extension set by compat::from_fastly_request_ref. + // Falls back to "unknown" when absent (e.g. local testing or unit tests). let client_ip = req - .get_client_ip_addr() + .extensions() + .get::() + .and_then(|ext| ext.0) .map(normalize_ip) .unwrap_or_else(|| "unknown".to_string()); @@ -105,8 +112,12 @@ pub fn generate_ec_id( /// # Errors /// /// - [`TrustedServerError::InvalidHeaderValue`] if cookie parsing fails -pub fn get_ec_id(req: &Request) -> Result, Report> { - if let Some(ec_id) = req.get_header(HEADER_X_TS_EC).and_then(|h| h.to_str().ok()) { +pub fn get_ec_id(req: &http::Request) -> Result, Report> { + if let Some(ec_id) = req + .headers() + .get(HEADER_X_TS_EC) + .and_then(|h| h.to_str().ok()) + { if ec_id_has_only_allowed_chars(ec_id) { log::trace!("Using existing EC ID from header: {}", ec_id); return Ok(Some(ec_id.to_string())); @@ -146,7 +157,7 @@ pub fn get_ec_id(req: &Request) -> Result, Report, ) -> Result> { if let Some(id) = get_ec_id(req)? { return Ok(id); @@ -161,7 +172,7 @@ pub fn get_or_generate_ec_id( #[cfg(test)] mod tests { use super::*; - use fastly::http::{HeaderName, HeaderValue}; + use http::header; use std::net::{Ipv4Addr, Ipv6Addr}; use crate::test_support::tests::create_test_settings; @@ -196,16 +207,16 @@ mod tests { assert_eq!(normalize_ip(ipv6_a), "2001:db8:abcd:1::"); } - fn create_test_request(headers: Vec<(HeaderName, &str)>) -> Request { - let mut req = Request::new("GET", "http://example.com"); - for (key, value) in headers { - req.set_header( - key, - HeaderValue::from_str(value).expect("should create valid header value"), - ); + fn create_test_request(headers: Vec<(&str, &str)>) -> http::Request { + let mut builder = http::Request::builder() + .method("GET") + .uri("http://example.com"); + for (name, value) in headers { + builder = builder.header(name, value); } - - req + builder + .body(Body::empty()) + .expect("should build test request") } fn is_ec_id_format(value: &str) -> bool { @@ -285,7 +296,7 @@ mod tests { #[test] fn test_get_ec_id_with_header() { let settings = create_test_settings(); - let req = create_test_request(vec![(HEADER_X_TS_EC, "existing_ec_id")]); + let req = create_test_request(vec![(HEADER_X_TS_EC.as_str(), "existing_ec_id")]); let ec_id = get_ec_id(&req).expect("should get EC ID"); assert_eq!(ec_id, Some("existing_ec_id".to_string())); @@ -298,7 +309,7 @@ mod tests { fn test_get_ec_id_with_cookie() { let settings = create_test_settings(); let req = create_test_request(vec![( - fastly::http::header::COOKIE, + header::COOKIE.as_str(), &format!("{}=existing_cookie_id", COOKIE_TS_EC), )]); @@ -328,9 +339,9 @@ mod tests { #[test] fn test_get_ec_id_rejects_invalid_header_and_falls_back_to_cookie() { let req = create_test_request(vec![ - (HEADER_X_TS_EC, "evil;injected"), + (HEADER_X_TS_EC.as_str(), "evil;injected"), ( - fastly::http::header::COOKIE, + header::COOKIE.as_str(), &format!("{}=valid_cookie_id", COOKIE_TS_EC), ), ]); @@ -346,7 +357,7 @@ mod tests { #[test] fn test_get_or_generate_ec_id_replaces_invalid_header() { let settings = create_test_settings(); - let req = create_test_request(vec![(HEADER_X_TS_EC, "evil;injected")]); + let req = create_test_request(vec![(HEADER_X_TS_EC.as_str(), "evil;injected")]); let ec_id = get_or_generate_ec_id(&settings, &req) .expect("should generate fresh ID on invalid header"); @@ -363,7 +374,7 @@ mod tests { #[test] fn test_get_ec_id_rejects_invalid_cookie() { let req = create_test_request(vec![( - fastly::http::header::COOKIE, + header::COOKIE.as_str(), &format!("{}=bad