diff --git a/crates/trusted-server-core/src/auction/README.md b/crates/trusted-server-core/src/auction/README.md index d6f4483a..bac2f3c3 100644 --- a/crates/trusted-server-core/src/auction/README.md +++ b/crates/trusted-server-core/src/auction/README.md @@ -257,18 +257,18 @@ The trusted-server handles several types of routes defined in `crates/trusted-se | Route | Method | Handler | Purpose | Line | |---------------------------|--------|--------------------------------|--------------------------------------------------|------| -| `/auction` | POST | `handle_auction()` | Main auction endpoint (Prebid.js/tsjs format) | 84 | -| `/first-party/proxy` | GET | `handle_first_party_proxy()` | Proxy creatives through first-party domain | 84 | -| `/first-party/click` | GET | `handle_first_party_click()` | Track clicks on ads | 85 | -| `/first-party/sign` | GET/POST | `handle_first_party_proxy_sign()` | Generate signed URLs for creatives | 86 | -| `/first-party/proxy-rebuild` | POST | `handle_first_party_proxy_rebuild()` | Rebuild creative HTML with new settings | 89 | -| `/static/tsjs=*` | GET | `handle_tsjs_dynamic()` | Serve tsjs library (Prebid.js alternative) | 66 | -| `/.well-known/ts.jwks.json` | GET | `handle_jwks_endpoint()` | Public key distribution for request signing | 71 | -| `/verify-signature` | POST | `handle_verify_signature()` | Verify signed requests | 74 | -| `/admin/keys/rotate` | POST | `handle_rotate_key()` | Rotate signing keys (admin only) | 77 | -| `/admin/keys/deactivate` | POST | `handle_deactivate_key()` | Deactivate signing keys (admin only) | 78 | -| `/integrations/*` | * | Integration Registry | Provider-specific endpoints (Prebid, etc.) | 92 | -| `*` (fallback) | * | `handle_publisher_request()` | Proxy to publisher origin | 108 | +| `/auction` | POST | `handle_auction()` | Main auction endpoint (Prebid.js/tsjs format) | 162 | +| `/first-party/proxy` | GET | `handle_first_party_proxy()` | Proxy creatives through first-party domain | 167 | +| `/first-party/click` | GET | `handle_first_party_click()` | Track clicks on ads | 170 | +| `/first-party/sign` | GET/POST | `handle_first_party_proxy_sign()` | Generate signed URLs for creatives | 173 | +| `/first-party/proxy-rebuild` | POST | `handle_first_party_proxy_rebuild()` | Rebuild creative HTML with new settings | 176 | +| `/static/tsjs=*` | GET | `handle_tsjs_dynamic()` | Serve tsjs library (Prebid.js alternative) | 145 | +| `/.well-known/trusted-server.json` | GET | `handle_trusted_server_discovery()` | Public key distribution for request signing | 149 | +| `/verify-signature` | POST | `handle_verify_signature()` | Verify signed requests | 154 | +| `/admin/keys/rotate` | POST | `handle_rotate_key()` | Rotate signing keys (admin only) | 158 | +| `/admin/keys/deactivate` | POST | `handle_deactivate_key()` | Deactivate signing keys (admin only) | 159 | +| `/integrations/*` | * | Integration Registry | Provider-specific endpoints (Prebid, etc.) | 179 | +| `*` (fallback) | * | `handle_publisher_request()` | Proxy to publisher origin | 195 | ### How Routing Works @@ -277,22 +277,50 @@ The Fastly Compute entrypoint uses pattern matching on `(Method, path)` tuples: ```rust let result = match (method, path.as_str()) { - // Auction endpoint + (Method::GET, path) if path.starts_with("/static/tsjs=") => { + handle_tsjs_dynamic(&req, integration_registry) + } + (Method::GET, "/.well-known/trusted-server.json") => { + handle_trusted_server_discovery(settings, runtime_services, req) + } + (Method::POST, "/verify-signature") => handle_verify_signature(settings, req), + (Method::POST, "/admin/keys/rotate") => handle_rotate_key(settings, req), + (Method::POST, "/admin/keys/deactivate") => handle_deactivate_key(settings, req), (Method::POST, "/auction") => { - handle_auction(&settings, &orchestrator, &runtime_services, req).await - }, - - // First-party endpoints - (Method::GET, "/first-party/proxy") => handle_first_party_proxy(&settings, req).await, - - // Integration registry (dynamic routes) - (m, path) if integration_registry.has_route(&m, path) => { - integration_registry.handle_proxy(&m, path, &settings, req).await + match runtime_services_for_consent_route(settings, runtime_services) { + Ok(auction_services) => { + handle_auction(settings, orchestrator, &auction_services, req).await + } + Err(e) => Err(e), + } + } + (Method::GET, "/first-party/proxy") => { + handle_first_party_proxy(settings, runtime_services, req).await + } + (Method::GET, "/first-party/click") => { + handle_first_party_click(settings, runtime_services, req).await + } + (Method::GET, "/first-party/sign") | (Method::POST, "/first-party/sign") => { + handle_first_party_proxy_sign(settings, runtime_services, req).await + } + (Method::POST, "/first-party/proxy-rebuild") => { + handle_first_party_proxy_rebuild(settings, runtime_services, req).await + } + (m, path) if integration_registry.has_route(&m, path) => integration_registry + .handle_proxy(&m, path, settings, runtime_services, req) + .await + .unwrap_or_else(|| { + Err(Report::new(TrustedServerError::BadRequest { + message: format!("Unknown integration route: {path}"), + })) + }), + _ => match runtime_services_for_consent_route(settings, runtime_services) { + Ok(publisher_services) => { + handle_publisher_request(settings, integration_registry, &publisher_services, req) + } + Err(e) => Err(e), }, - - // Fallback to publisher origin - _ => handle_publisher_request(&settings, &integration_registry, &runtime_services, req), -} +}; ``` #### 2. Integration Registry (Dynamic Routes) diff --git a/crates/trusted-server-core/src/auction/endpoints.rs b/crates/trusted-server-core/src/auction/endpoints.rs index e272d761..4ce4440e 100644 --- a/crates/trusted-server-core/src/auction/endpoints.rs +++ b/crates/trusted-server-core/src/auction/endpoints.rs @@ -8,7 +8,6 @@ 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; use crate::settings::Settings; @@ -32,7 +31,7 @@ use super::AuctionOrchestrator; pub async fn handle_auction( settings: &Settings, orchestrator: &AuctionOrchestrator, - runtime_services: &RuntimeServices, + services: &RuntimeServices, mut req: Request, ) -> Result> { // Parse request body @@ -49,15 +48,21 @@ 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 = get_or_generate_ec_id(settings, services, &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)?; - #[allow(deprecated)] - let geo = GeoInfo::from_request(&req); + let geo = services + .geo() + .lookup(services.client_info.client_ip) + .unwrap_or_else(|e| { + log::warn!("geo lookup failed: {e}"); + None + }); let consent_context = consent::build_consent_context(&consent::ConsentPipelineInput { jar: cookie_jar.as_ref(), req: &req, @@ -68,24 +73,32 @@ pub async fn handle_auction( .consent .consent_store .as_deref() - .map(|_| runtime_services.kv_store()), + .map(|_| services.kv_store()), }); // Convert tsjs request format to auction request - let auction_request = - convert_tsjs_to_auction_request(&body, settings, &req, consent_context, &ec_id)?; + let auction_request = convert_tsjs_to_auction_request( + &body, + settings, + services, + &req, + consent_context, + &ec_id, + geo, + )?; // Create auction context let context = AuctionContext { settings, request: &req, + client_info: &services.client_info, timeout_ms: settings.auction.timeout_ms, provider_responses: None, }; // Run the auction let result = orchestrator - .run_auction(&auction_request, &context, runtime_services) + .run_auction(&auction_request, &context, services) .await .change_context(TrustedServerError::Auction { message: "Auction orchestration failed".to_string(), diff --git a/crates/trusted-server-core/src/auction/formats.rs b/crates/trusted-server-core/src/auction/formats.rs index 1f557a17..5237921a 100644 --- a/crates/trusted-server-core/src/auction/formats.rs +++ b/crates/trusted-server-core/src/auction/formats.rs @@ -18,8 +18,8 @@ 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}; +use crate::platform::{GeoInfo, RuntimeServices}; use crate::settings::Settings; use super::orchestrator::OrchestrationResult; @@ -83,14 +83,17 @@ pub struct BannerUnit { pub fn convert_tsjs_to_auction_request( body: &AdRequest, settings: &Settings, + services: &RuntimeServices, req: &Request, consent: ConsentContext, ec_id: &str, + geo: Option, ) -> 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 = + generate_ec_id(settings, services).change_context(TrustedServerError::Auction { + message: "Failed to generate fresh EC ID".to_string(), + })?; // Convert ad units to slots let mut slots = Vec::new(); @@ -137,9 +140,8 @@ pub fn convert_tsjs_to_auction_request( user_agent: req .get_header_str("user-agent") .map(std::string::ToString::to_string), - ip: req.get_client_ip_addr().map(|ip| ip.to_string()), - #[allow(deprecated)] - geo: GeoInfo::from_request(req), + ip: services.client_info.client_ip.map(|ip| ip.to_string()), + geo, }); // Forward allowed config entries from the JS request into the context map. diff --git a/crates/trusted-server-core/src/auction/orchestrator.rs b/crates/trusted-server-core/src/auction/orchestrator.rs index 8ee6e13a..0b650e07 100644 --- a/crates/trusted-server-core/src/auction/orchestrator.rs +++ b/crates/trusted-server-core/src/auction/orchestrator.rs @@ -145,6 +145,7 @@ impl AuctionOrchestrator { let mediator_context = AuctionContext { settings: context.settings, request: context.request, + client_info: context.client_info, timeout_ms: remaining_ms, provider_responses: Some(&provider_responses), }; @@ -329,6 +330,7 @@ impl AuctionOrchestrator { let provider_context = AuctionContext { settings: context.settings, request: context.request, + client_info: context.client_info, timeout_ms: effective_timeout, provider_responses: context.provider_responses, }; @@ -695,10 +697,12 @@ mod tests { fn create_test_context<'a>( settings: &'a crate::settings::Settings, req: &'a Request, + client_info: &'a crate::platform::ClientInfo, ) -> AuctionContext<'a> { AuctionContext { settings, request: req, + client_info, timeout_ms: 2000, provider_responses: None, } @@ -791,7 +795,15 @@ mod tests { let request = create_test_auction_request(); let settings = create_test_settings(); let req = Request::get("https://test.com/test"); - let context = create_test_context(&settings, &req); + let context = create_test_context( + &settings, + &req, + &crate::platform::ClientInfo { + client_ip: None, + tls_protocol: None, + tls_cipher: None, + }, + ); let result = orchestrator .run_auction(&request, &context, &noop_services()) diff --git a/crates/trusted-server-core/src/auction/types.rs b/crates/trusted-server-core/src/auction/types.rs index aa863d61..82538206 100644 --- a/crates/trusted-server-core/src/auction/types.rs +++ b/crates/trusted-server-core/src/auction/types.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use crate::auction::context::ContextValue; use crate::geo::GeoInfo; +use crate::platform::ClientInfo; use crate::settings::Settings; /// Represents a unified auction request across all providers. @@ -102,6 +103,7 @@ pub struct SiteInfo { pub struct AuctionContext<'a> { pub settings: &'a Settings, pub request: &'a Request, + pub client_info: &'a ClientInfo, pub timeout_ms: u32, /// Provider responses from the bidding phase, used by mediators. /// This is `None` for regular bidders and `Some` when calling a mediator. diff --git a/crates/trusted-server-core/src/edge_cookie.rs b/crates/trusted-server-core/src/edge_cookie.rs index 063c3fdd..7d2094e3 100644 --- a/crates/trusted-server-core/src/edge_cookie.rs +++ b/crates/trusted-server-core/src/edge_cookie.rs @@ -14,6 +14,7 @@ use sha2::Sha256; 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; +use crate::platform::RuntimeServices; use crate::settings::Settings; type HmacSha256 = Hmac; @@ -67,12 +68,13 @@ fn generate_random_suffix(length: usize) -> String { /// - [`TrustedServerError::Ec`] if HMAC generation fails pub fn generate_ec_id( settings: &Settings, - req: &Request, + services: &RuntimeServices, ) -> 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. - let client_ip = req - .get_client_ip_addr() + let client_ip = services + .client_info + .client_ip .map(normalize_ip) .unwrap_or_else(|| "unknown".to_string()); @@ -146,6 +148,7 @@ pub fn get_ec_id(req: &Request) -> Result, Report Result> { if let Some(id) = get_ec_id(req)? { @@ -153,7 +156,7 @@ pub fn get_or_generate_ec_id( } // If no existing EC ID found, generate a fresh one - let ec_id = generate_ec_id(settings, req)?; + let ec_id = generate_ec_id(settings, services)?; log::trace!("No existing EC ID, generated: {}", ec_id); Ok(ec_id) } @@ -164,6 +167,7 @@ mod tests { use fastly::http::{HeaderName, HeaderValue}; use std::net::{Ipv4Addr, Ipv6Addr}; + use crate::platform::test_support::{noop_services, noop_services_with_client_ip}; use crate::test_support::tests::create_test_settings; #[test] @@ -236,9 +240,8 @@ mod tests { #[test] fn test_generate_ec_id() { let settings: Settings = create_test_settings(); - let req = create_test_request(vec![]); - let ec_id = generate_ec_id(&settings, &req).expect("should generate EC ID"); + let ec_id = generate_ec_id(&settings, &noop_services()).expect("should generate EC ID"); log::debug!("Generated EC ID: {}", ec_id); assert!( is_ec_id_format(&ec_id), @@ -246,6 +249,25 @@ mod tests { ); } + #[test] + fn test_generate_ec_id_uses_client_ip() { + let settings = create_test_settings(); + let ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)); + + let id_with_ip = generate_ec_id(&settings, &noop_services_with_client_ip(ip)) + .expect("should generate EC ID with client IP"); + let id_without_ip = generate_ec_id(&settings, &noop_services()) + .expect("should generate EC ID without client IP"); + + let hmac_with_ip = id_with_ip.split_once('.').expect("should contain dot").0; + let hmac_without_ip = id_without_ip.split_once('.').expect("should contain dot").0; + + assert_ne!( + hmac_with_ip, hmac_without_ip, + "should produce different HMAC when client IP differs" + ); + } + #[test] fn test_is_ec_id_format_accepts_valid_value() { let value = format!("{}.{}", "a".repeat(64), "Ab12z9"); @@ -290,7 +312,8 @@ mod tests { let ec_id = get_ec_id(&req).expect("should get EC ID"); assert_eq!(ec_id, Some("existing_ec_id".to_string())); - let ec_id = get_or_generate_ec_id(&settings, &req).expect("should reuse header EC ID"); + let ec_id = get_or_generate_ec_id(&settings, &noop_services(), &req) + .expect("should reuse header EC ID"); assert_eq!(ec_id, "existing_ec_id"); } @@ -305,7 +328,8 @@ mod tests { let ec_id = get_ec_id(&req).expect("should get EC ID"); assert_eq!(ec_id, Some("existing_cookie_id".to_string())); - let ec_id = get_or_generate_ec_id(&settings, &req).expect("should reuse cookie EC ID"); + let ec_id = get_or_generate_ec_id(&settings, &noop_services(), &req) + .expect("should reuse cookie EC ID"); assert_eq!(ec_id, "existing_cookie_id"); } @@ -321,7 +345,8 @@ mod tests { let settings = create_test_settings(); let req = create_test_request(vec![]); - let ec_id = get_or_generate_ec_id(&settings, &req).expect("should get or generate EC ID"); + let ec_id = get_or_generate_ec_id(&settings, &noop_services(), &req) + .expect("should get or generate EC ID"); assert!(!ec_id.is_empty()); } @@ -348,7 +373,7 @@ mod tests { let settings = create_test_settings(); let req = create_test_request(vec![(HEADER_X_TS_EC, "evil;injected")]); - let ec_id = get_or_generate_ec_id(&settings, &req) + let ec_id = get_or_generate_ec_id(&settings, &noop_services(), &req) .expect("should generate fresh ID on invalid header"); assert_ne!( ec_id, "evil;injected", diff --git a/crates/trusted-server-core/src/html_processor.rs b/crates/trusted-server-core/src/html_processor.rs index 540ab29d..ab5ff72a 100644 --- a/crates/trusted-server-core/src/html_processor.rs +++ b/crates/trusted-server-core/src/html_processor.rs @@ -1,6 +1,22 @@ -//! Simplified HTML processor that combines URL replacement and integration injection +//! Simplified HTML processor that combines URL replacement and integration injection. //! -//! This module provides a `StreamProcessor` implementation for HTML content. +//! This module provides a [`StreamProcessor`] implementation for HTML content. +//! It handles `