diff --git a/.env.dev b/.env.dev index dc7a6f2..4272f2f 100644 --- a/.env.dev +++ b/.env.dev @@ -1,6 +1,3 @@ -# [ad_server] -TRUSTED_SERVER__AD_SERVER__AD_PARTNER_URL=http://127.0.0.1:10180 - # [publisher] TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=http://localhost:9090 diff --git a/Cargo.lock b/Cargo.lock index 83fa816..4204422 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1493,6 +1493,28 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -2086,6 +2108,7 @@ dependencies = [ "url", "urlencoding", "uuid", + "validator", ] [[package]] @@ -2195,6 +2218,36 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 4fe101e..e047612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,4 +48,5 @@ toml = "0.9.0" url = "2.4.1" urlencoding = "2.1" uuid = { version = "1.0", features = ["v4"] } +validator = { version = "0.20", features = ["derive"] } which = "8" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index a80b208..05ed9c7 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -11,6 +11,8 @@ license = "Apache-2.0" [dependencies] base64 = { workspace = true } brotli = { workspace = true } +bytes = { workspace = true } +chacha20poly1305 = { workspace = true } chrono = { workspace = true } config = { workspace = true } cookie = { workspace = true } @@ -22,31 +24,31 @@ futures = { workspace = true } handlebars = { workspace = true } hex = { workspace = true } hmac = { workspace = true } -chacha20poly1305 = { workspace = true } http = { workspace = true } log = { workspace = true } log-fastly = { workspace = true } +lol_html = { workspace = true } +pin-project-lite = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } tokio = { workspace = true } -bytes = { workspace = true } +trusted-server-js = { path = "../js" } url = { workspace = true } -uuid = { workspace = true } urlencoding = { workspace = true } -lol_html = { workspace = true } -pin-project-lite = { workspace = true } -trusted-server-js = { path = "../js" } +uuid = { workspace = true } +validator = { workspace = true } [build-dependencies] -serde = { workspace = true } -serde_json = { workspace = true } config = { workspace = true } derive_more = { workspace = true } error-stack = { workspace = true } http = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } toml = { workspace = true } url = { workspace = true } +validator = { workspace = true } [features] default = [] diff --git a/crates/common/src/advertiser.rs b/crates/common/src/advertiser.rs deleted file mode 100644 index 3e369d0..0000000 --- a/crates/common/src/advertiser.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! Ad serving and advertiser integration functionality. -//! -//! This module handles ad requests, including GDPR consent checking, -//! synthetic ID generation, visitor tracking, and communication with -//! external ad partners. - -use std::env; - -use error_stack::Report; -use fastly::http::{header, StatusCode}; -use fastly::{KVStore, Request, Response}; - -use crate::constants::{ - HEADER_X_COMPRESS_HINT, HEADER_X_CONSENT_ADVERTISING, HEADER_X_FORWARDED_FOR, - HEADER_X_GEO_CITY, HEADER_X_GEO_CONTINENT, HEADER_X_GEO_COORDINATES, HEADER_X_GEO_COUNTRY, - HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE, -}; -use crate::error::TrustedServerError; -use crate::gdpr::{get_consent_from_request, GdprConsent}; -use crate::geo::get_dma_code; -use crate::models::AdResponse; -use crate::settings::Settings; -use crate::synthetic::generate_synthetic_id; - -/// Handles ad creative requests. -/// -/// Processes ad requests with synthetic ID and consent checking. -/// -/// # Errors -/// -/// Returns a [`TrustedServerError`] if: -/// - Synthetic ID generation fails -/// - Backend communication fails -/// - Response creation fails -pub fn handle_ad_request( - settings: &Settings, - mut req: Request, -) -> Result> { - // Check GDPR consent to determine if we should serve personalized or non-personalized ads - let _consent = match get_consent_from_request(&req) { - Some(c) => c, - None => { - log::debug!("No GDPR consent found in ad request, using default"); - GdprConsent::default() - } - }; - let advertising_consent = req - .get_header(HEADER_X_CONSENT_ADVERTISING) - .and_then(|h| h.to_str().ok()) - .map(|v| v == "true") - .unwrap_or(false); - - // Add DMA code extraction - let dma_code = get_dma_code(&mut req); - - log::info!("Client location - DMA Code: {:?}", dma_code); - - // Log headers for debugging - let client_ip = req - .get_client_ip_addr() - .map(|ip| ip.to_string()) - .unwrap_or_else(|| "Unknown".to_string()); - let x_forwarded_for = req - .get_header(HEADER_X_FORWARDED_FOR) - .map(|h| h.to_str().unwrap_or("Unknown")); - - log::info!("Client IP: {}", client_ip); - log::info!("X-Forwarded-For: {}", x_forwarded_for.unwrap_or("None")); - log::info!("Advertising consent: {}", advertising_consent); - - // Generate synthetic ID only if we have consent - let synthetic_id = if advertising_consent { - generate_synthetic_id(settings, &req)? - } else { - // Use a generic ID for non-personalized ads - "non-personalized".to_string() - }; - - // Only track visits if we have consent - if advertising_consent { - // Increment visit counter in KV store - log::info!("Opening KV store: {}", settings.synthetic.counter_store); - if let Ok(Some(store)) = KVStore::open(settings.synthetic.counter_store.as_str()) { - log::info!("Fetching current count for synthetic ID: {}", synthetic_id); - let current_count: i32 = store - .lookup(&synthetic_id) - .map(|mut val| match String::from_utf8(val.take_body_bytes()) { - Ok(s) => { - log::info!("Value from KV store: {}", s); - Some(s) - } - Err(e) => { - log::error!("Error converting bytes to string: {}", e); - None - } - }) - .map(|opt_s| { - log::info!("Parsing string value: {:?}", opt_s); - opt_s.and_then(|s| s.parse().ok()) - }) - .unwrap_or_else(|_| { - log::info!("No existing count found, starting at 0"); - None - }) - .unwrap_or(0); - - let new_count = current_count + 1; - log::info!("Incrementing count from {} to {}", current_count, new_count); - - if let Err(e) = store.insert(&synthetic_id, new_count.to_string().as_bytes()) { - log::error!("Error updating KV store: {:?}", e); - } - } - } - - // Modify the ad server URL construction to include DMA code if available - let ad_server_url = if advertising_consent { - let mut url = settings - .ad_server - .sync_url - .replace("{{synthetic_id}}", &synthetic_id); - if let Some(dma) = dma_code { - url = format!("{}&dma={}", url, dma); - } - url - } else { - // Use a different URL or parameter for non-personalized ads - settings - .ad_server - .sync_url - .replace("{{synthetic_id}}", "non-personalized") - }; - - log::info!("Sending request to backend: {}", ad_server_url); - - // Add header logging here - let mut ad_req = Request::get(ad_server_url); - - // Add consent information to the ad request - ad_req.set_header( - HEADER_X_CONSENT_ADVERTISING, - if advertising_consent { "true" } else { "false" }, - ); - - log::info!("Request headers to Equativ:"); - for (name, value) in ad_req.get_headers() { - log::info!(" {}: {:?}", name, value); - } - - match ad_req.send(settings.ad_server.ad_partner_url.as_str()) { - Ok(mut res) => { - log::info!( - "Received response from backend with status: {}", - res.get_status() - ); - - // Extract Fastly PoP from the Compute environment - let fastly_pop = env::var("FASTLY_POP").unwrap_or_else(|_| "unknown".to_string()); - let fastly_cache_generation = - env::var("FASTLY_CACHE_GENERATION").unwrap_or_else(|_| "unknown".to_string()); - let fastly_customer_id = - env::var("FASTLY_CUSTOMER_ID").unwrap_or_else(|_| "unknown".to_string()); - let fastly_hostname = - env::var("FASTLY_HOSTNAME").unwrap_or_else(|_| "unknown".to_string()); - let fastly_region = env::var("FASTLY_REGION").unwrap_or_else(|_| "unknown".to_string()); - let fastly_service_id = - env::var("FASTLY_SERVICE_ID").unwrap_or_else(|_| "unknown".to_string()); - let fastly_trace_id = - env::var("FASTLY_TRACE_ID").unwrap_or_else(|_| "unknown".to_string()); - - log::info!("Fastly POP: {}", fastly_pop); - log::info!("Fastly Compute Variables:"); - log::info!(" - FASTLY_CACHE_GENERATION: {}", fastly_cache_generation); - log::info!(" - FASTLY_CUSTOMER_ID: {}", fastly_customer_id); - log::info!(" - FASTLY_HOSTNAME: {}", fastly_hostname); - log::info!(" - FASTLY_POP: {}", fastly_pop); - log::info!(" - FASTLY_REGION: {}", fastly_region); - log::info!(" - FASTLY_SERVICE_ID: {}", fastly_service_id); - //log::info!(" - FASTLY_SERVICE_VERSION: {}", fastly_service_version); - log::info!(" - FASTLY_TRACE_ID: {}", fastly_trace_id); - - // Log all response headers - log::info!("Response headers from Equativ:"); - for (name, value) in res.get_headers() { - log::info!(" {}: {:?}", name, value); - } - - if res.get_status().is_success() { - let body = res.take_body_str(); - log::info!("Backend response body: {}", body); - - // Parse the JSON response and extract opid - if let Ok(ad_response) = serde_json::from_str::(&body) { - // Look for the callback with type "impression" - if let Some(callback) = ad_response - .callbacks - .iter() - .find(|c| c.callback_type == "impression") - { - // Extract opid from the URL - if let Some(opid) = callback - .url - .split('&') - .find(|¶m| param.starts_with("opid=")) - .and_then(|param| param.split('=').nth(1)) - { - log::info!("Found opid: {}", opid); - - // Store in opid KV store - log::info!( - "Attempting to open KV store: {}", - settings.synthetic.opid_store - ); - match KVStore::open(settings.synthetic.opid_store.as_str()) { - Ok(Some(store)) => { - log::info!("Successfully opened KV store"); - match store.insert(&synthetic_id, opid.as_bytes()) { - Ok(_) => log::info!( - "Successfully stored opid {} for synthetic ID: {}", - opid, - synthetic_id - ), - Err(e) => { - log::error!("Error storing opid in KV store: {:?}", e) - } - } - } - Ok(None) => { - log::warn!( - "KV store returned None: {}", - settings.synthetic.opid_store - ); - } - Err(e) => { - log::error!( - "Error opening KV store '{}': {:?}", - settings.synthetic.opid_store, - e - ); - } - }; - } - } - } - - // Return the JSON response with CORS headers - let mut response = Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_header(header::CACHE_CONTROL, "no-store, private") - .with_header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .with_header( - header::ACCESS_CONTROL_EXPOSE_HEADERS, - "X-Geo-City, X-Geo-Country, X-Geo-Continent, X-Geo-Coordinates, X-Geo-Metro-Code, X-Geo-Info-Available" - ) - .with_header(HEADER_X_COMPRESS_HINT, "on") - .with_body(body); - - // Copy geo headers from request to response - for header_name in &[ - HEADER_X_GEO_CITY, - HEADER_X_GEO_COUNTRY, - HEADER_X_GEO_CONTINENT, - HEADER_X_GEO_COORDINATES, - HEADER_X_GEO_METRO_CODE, - HEADER_X_GEO_INFO_AVAILABLE, - ] { - if let Some(value) = req.get_header(header_name) { - response.set_header(header_name, value); - } - } - - Ok(response) - } else { - log::warn!("Backend returned non-success status"); - Ok(Response::from_status(StatusCode::NO_CONTENT) - .with_header(header::CONTENT_TYPE, "application/json") - .with_header(HEADER_X_COMPRESS_HINT, "on") - .with_body("{}")) - } - } - Err(e) => { - log::error!("Error making backend request: {:?}", e); - Ok(Response::from_status(StatusCode::NO_CONTENT) - .with_header(header::CONTENT_TYPE, "application/json") - .with_header(HEADER_X_COMPRESS_HINT, "on") - .with_body("{}")) - } - } -} diff --git a/crates/common/src/gam.rs b/crates/common/src/gam.rs deleted file mode 100644 index 04066fe..0000000 --- a/crates/common/src/gam.rs +++ /dev/null @@ -1,1382 +0,0 @@ -use error_stack::Report; -use fastly::http::{header, Method, StatusCode}; -use fastly::{Request, Response}; -use serde_json::json; -use std::collections::HashMap; -use std::io::Read; -use uuid::Uuid; - -use crate::error::TrustedServerError; -use crate::gdpr::get_consent_from_request; -use crate::settings::Settings; -use crate::templates::GAM_TEST_TEMPLATE; - -/// GAM request builder for server-side ad requests -pub struct GamRequest { - pub publisher_id: String, - pub ad_units: Vec, - pub page_url: String, - pub correlator: String, - pub prmtvctx: Option, // Permutive context - initially hardcoded, then dynamic - pub user_agent: String, - pub synthetic_id: String, -} - -impl GamRequest { - /// Create a new GAM request with default parameters - pub fn new(settings: &Settings, req: &Request) -> Result> { - let correlator = Uuid::new_v4().to_string(); - let page_url = req.get_url().to_string(); - let user_agent = req - .get_header(header::USER_AGENT) - .and_then(|h| h.to_str().ok()) - .unwrap_or("Mozilla/5.0 (compatible; TrustedServer/1.0)") - .to_string(); - - // Get synthetic ID from request headers - let synthetic_id = req - .get_header("X-Synthetic-Trusted-Server") - .and_then(|h| h.to_str().ok()) - .unwrap_or("unknown") - .to_string(); - - Ok(Self { - publisher_id: settings.gam.publisher_id.clone(), - ad_units: settings - .gam - .ad_units - .iter() - .map(|u| u.name.clone()) - .collect(), - page_url, - correlator, - prmtvctx: None, // Will be set later with captured value - user_agent, - synthetic_id, - }) - } - - /// Set the Permutive context (initially hardcoded from captured request) - pub fn with_prmtvctx(mut self, prmtvctx: String) -> Self { - self.prmtvctx = Some(prmtvctx); - self - } - - /// Build the GAM request URL for the "Golden URL" replay phase - pub fn build_golden_url(&self) -> String { - // This will be replaced with the actual captured URL from test-publisher.com - // For now, using a template based on the captured Golden URL - let mut params = HashMap::new(); - - // Core GAM parameters (based on captured URL) - params.insert("pvsid".to_string(), "3290837576990024".to_string()); // Publisher Viewability ID - params.insert("correlator".to_string(), self.correlator.clone()); - params.insert( - "eid".to_string(), - "31086815,31093089,95353385,31085777,83321072".to_string(), - ); // Event IDs - params.insert("output".to_string(), "ldjh".to_string()); // Important: not 'json' - params.insert("gdfp_req".to_string(), "1".to_string()); - params.insert("vrg".to_string(), "202506170101".to_string()); // Version/Region - params.insert("ptt".to_string(), "17".to_string()); // Page Type - params.insert("impl".to_string(), "fifs".to_string()); // Implementation - - // Ad unit parameters (simplified version of captured format) - params.insert( - "iu_parts".to_string(), - format!("{},{},homepage", self.publisher_id, "trustedserver"), - ); - params.insert( - "enc_prev_ius".to_string(), - "/0/1/2,/0/1/2,/0/1/2".to_string(), - ); - params.insert("prev_iu_szs".to_string(), "320x50|300x250|728x90|970x90|970x250|1x2,320x50|300x250|728x90|970x90|970x250|1x2,320x50|300x250|728x90|970x90|970x250|1x2".to_string()); - params.insert("fluid".to_string(), "height,height,height".to_string()); - - // Browser context (simplified) - params.insert("biw".to_string(), "1512".to_string()); - params.insert("bih".to_string(), "345".to_string()); - params.insert("u_tz".to_string(), "-300".to_string()); - params.insert("u_cd".to_string(), "30".to_string()); - params.insert("u_sd".to_string(), "2".to_string()); - - // Page context - params.insert("url".to_string(), self.page_url.clone()); - params.insert( - "dt".to_string(), - chrono::Utc::now().timestamp_millis().to_string(), - ); - - // Add Permutive context if available (in cust_params like the captured URL) - if let Some(ref prmtvctx) = self.prmtvctx { - let cust_params = format!("permutive={}&puid={}", prmtvctx, self.synthetic_id); - params.insert("cust_params".to_string(), cust_params); - } - - // Build query string - let query_string = params - .iter() - .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v))) - .collect::>() - .join("&"); - - format!("{}?{}", self.get_base_url(), query_string) - } - - /// Get the base GAM server URL - pub fn get_base_url(&self) -> String { - // This will be updated with the actual GAM endpoint from captured request - "https://securepubads.g.doubleclick.net/gampad/ads".to_string() - } - - /// Send the GAM request and return the response - pub async fn send_request( - &self, - _settings: &Settings, - ) -> Result> { - let url = self.build_golden_url(); - log::info!("Sending GAM request to: {}", url); - - // Create the request - let mut req = Request::new(Method::GET, &url); - - // Set headers to mimic a browser request (using only Fastly-compatible headers) - req.set_header(header::USER_AGENT, &self.user_agent); - req.set_header(header::ACCEPT, "application/json, text/plain, */*"); - req.set_header(header::ACCEPT_LANGUAGE, "en-US,en;q=0.9"); - req.set_header(header::ACCEPT_ENCODING, "gzip, deflate, br"); - req.set_header(header::REFERER, &self.page_url); - req.set_header(header::ORIGIN, &self.page_url); - req.set_header("X-Synthetic-ID", &self.synthetic_id); - - // Send the request to the GAM backend - let backend_name = "gam_backend"; - log::info!("Sending request to backend: {}", backend_name); - - match req.send(backend_name) { - Ok(mut response) => { - log::info!( - "Received GAM response with status: {}", - response.get_status() - ); - - // Log response headers for debugging - log::debug!("GAM Response headers:"); - for (name, value) in response.get_headers() { - log::debug!(" {}: {:?}", name, value); - } - - // Handle response body safely - let body_bytes = response.take_body_bytes(); - let body = match std::str::from_utf8(&body_bytes) { - Ok(body_str) => body_str.to_string(), - Err(e) => { - log::warn!("Could not read response body as UTF-8: {:?}", e); - - // Try to decompress if it's Brotli compressed - let mut decompressed = Vec::new(); - match brotli::BrotliDecompress( - &mut std::io::Cursor::new(&body_bytes), - &mut decompressed, - ) { - Ok(_) => match std::str::from_utf8(&decompressed) { - Ok(decompressed_str) => { - log::debug!( - "Successfully decompressed Brotli response: {} bytes", - decompressed_str.len() - ); - decompressed_str.to_string() - } - Err(e2) => { - log::warn!( - "Could not read decompressed body as UTF-8: {:?}", - e2 - ); - format!("{{\"error\": \"decompression_failed\", \"message\": \"Could not decode decompressed response\", \"original_error\": \"{:?}\"}}", e2) - } - }, - Err(e2) => { - log::warn!("Could not decompress Brotli response: {:?}", e2); - // Return a placeholder since we can't parse the binary response - format!("{{\"error\": \"compression_failed\", \"message\": \"Could not decompress response\", \"original_error\": \"{:?}\"}}", e2) - } - } - } - }; - - log::debug!("GAM Response body length: {} bytes", body.len()); - - // For debugging, log first 500 chars of response - if body.len() > 500 { - log::debug!("GAM Response preview: {}...", &body[..500]); - } else { - log::debug!("GAM Response: {}", body); - } - - Ok(Response::from_status(response.get_status()) - .with_header(header::CONTENT_TYPE, "text/plain") - .with_header(header::CACHE_CONTROL, "no-store, private") - .with_header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .with_header("X-GAM-Test", "true") - .with_header("X-Synthetic-ID", &self.synthetic_id) - .with_header("X-Correlator", &self.correlator) - .with_header("x-compress-hint", "on") - .with_body(body)) - } - Err(e) => { - log::error!("Error sending GAM request: {:?}", e); - Err(Report::new(TrustedServerError::Gam { - message: format!("Failed to send GAM request: {}", e), - })) - } - } - } -} - -/// Handle GAM test requests (Phase 1: Capture & Replay) -pub async fn handle_gam_test( - settings: &Settings, - req: Request, -) -> Result> { - log::info!("Starting GAM test request handling"); - - // Debug: Log all request headers - log::debug!("GAM Test - All request headers:"); - for (name, value) in req.get_headers() { - log::debug!(" {}: {:?}", name, value); - } - - // Check consent status from cookie (more reliable than header) - let consent = get_consent_from_request(&req).unwrap_or_default(); - let advertising_consent = consent.advertising; - - log::debug!("GAM Test - Consent from cookie: {:?}", consent); - log::debug!( - "GAM Test - Advertising consent from cookie: {}", - advertising_consent - ); - - // Also check header as fallback - let header_consent = req - .get_header("X-Consent-Advertising") - .and_then(|h| h.to_str().ok()) - .map(|v| v == "true") - .unwrap_or(false); - - log::debug!( - "GAM Test - Advertising consent from header: {}", - header_consent - ); - - // Use cookie consent as primary, header as fallback - let final_consent = advertising_consent || header_consent; - log::info!("GAM Test - Final advertising consent: {}", final_consent); - - if !final_consent { - return Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "No advertising consent", - "message": "GAM requests require advertising consent", - "debug": { - "cookie_consent": consent, - "header_consent": header_consent, - "final_consent": final_consent - } - })) - .map_err(|e| { - Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize consent response: {}", e), - }) - }); - } - - // Create GAM request - let gam_req = match GamRequest::new(settings, &req) { - Ok(req) => { - log::info!("Successfully created GAM request"); - req - } - Err(e) => { - log::error!("Error creating GAM request: {:?}", e); - return Response::from_status(StatusCode::INTERNAL_SERVER_ERROR) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "Failed to create GAM request", - "details": format!("{:?}", e) - })) - .map_err(|e| { - Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize error response: {}", e), - }) - }); - } - }; - - // For Phase 1, we'll use a hardcoded prmtvctx value from captured request - // This will be replaced with the actual value from test-publisher.com - let gam_req_with_context = gam_req.with_prmtvctx("129627,137412,138272,139095,139096,139218,141364,143196,143210,143211,143214,143217,144331,144409,144438,144444,144488,144543,144663,144679,144731,144824,144916,145933,146347,146348,146349,146350,146351,146370,146383,146391,146392,146393,146424,146995,147077,147740,148616,148627,148628,149007,150420,150663,150689,150690,150692,150752,150753,150755,150756,150757,150764,150770,150781,150862,154609,155106,155109,156204,164183,164573,165512,166017,166019,166484,166486,166487,166488,166492,166494,166495,166497,166511,167639,172203,172544,173548,176066,178053,178118,178120,178121,178133,180321,186069,199642,199691,202074,202075,202081,233782,238158,adv,bhgp,bhlp,bhgw,bhlq,bhlt,bhgx,bhgv,bhgu,bhhb,rts".to_string()); - - log::info!( - "Sending GAM request with correlator: {}", - gam_req_with_context.correlator - ); - - match gam_req_with_context.send_request(settings).await { - Ok(response) => { - log::info!("GAM request successful"); - Ok(response) - } - Err(e) => { - log::error!("GAM request failed: {:?}", e); - Response::from_status(StatusCode::INTERNAL_SERVER_ERROR) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "Failed to send GAM request", - "details": format!("{:?}", e) - })) - .map_err(|e| { - Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize error response: {}", e), - }) - }) - } - } -} - -/// Handle GAM golden URL replay (for testing captured requests) -pub async fn handle_gam_golden_url( - _settings: &Settings, - _req: Request, -) -> Result> { - log::info!("Handling GAM golden URL replay"); - - // This endpoint will be used to test the exact captured URL from test-publisher.com - // For now, return a placeholder response - Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "status": "golden_url_replay", - "message": "Ready for captured URL testing", - "next_steps": [ - "1. Capture complete GAM request URL from test-publisher.com", - "2. Replace placeholder URL in GamRequest::build_golden_url()", - "3. Test with exact captured parameters" - ] - })) - .map_err(|e| { - Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize response: {}", e), - }) - }) -} - -/// Handle GAM custom URL testing (for testing captured URLs directly) -pub async fn handle_gam_custom_url( - _settings: &Settings, - mut req: Request, -) -> Result> { - log::info!("Handling GAM custom URL test"); - - // Check consent status from cookie or header for testing - let consent = get_consent_from_request(&req).unwrap_or_default(); - let cookie_consent = consent.advertising; - - // Also check header as fallback for testing - let header_consent = req - .get_header("X-Consent-Advertising") - .and_then(|h| h.to_str().ok()) - .map(|v| v == "true") - .unwrap_or(false); - - let advertising_consent = cookie_consent || header_consent; - - if !advertising_consent { - return Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "No advertising consent", - "message": "GAM requests require advertising consent" - })) - .map_err(|e| { - Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize response: {}", e), - }) - }); - } - - // Parse the request body to get the custom URL - let body = req.take_body_str(); - let url_data: serde_json::Value = serde_json::from_str(&body).map_err(|e| { - log::error!("Error parsing request body: {:?}", e); - Report::new(TrustedServerError::Gam { - message: format!("Invalid JSON in request body: {}", e), - }) - })?; - - let custom_url = url_data["url"].as_str().ok_or_else(|| { - Report::new(TrustedServerError::Gam { - message: "Missing 'url' field in request body".to_string(), - }) - })?; - - log::info!("Testing custom GAM URL: {}", custom_url); - - // Create a request to the custom URL - let mut gam_req = Request::new(Method::GET, custom_url); - - // Set headers to mimic a browser request - gam_req.set_header( - header::USER_AGENT, - "Mozilla/5.0 (compatible; TrustedServer/1.0)", - ); - gam_req.set_header(header::ACCEPT, "application/json, text/plain, */*"); - gam_req.set_header(header::ACCEPT_LANGUAGE, "en-US,en;q=0.9"); - gam_req.set_header(header::ACCEPT_ENCODING, "gzip, deflate, br"); - gam_req.set_header(header::REFERER, "https://www.test-publisher.com/"); - gam_req.set_header(header::ORIGIN, "https://www.test-publisher.com"); - - // Send the request to the GAM backend - let backend_name = "gam_backend"; - log::info!("Sending custom URL request to backend: {}", backend_name); - - match gam_req.send(backend_name) { - Ok(mut response) => { - log::info!( - "Received GAM response with status: {}", - response.get_status() - ); - - // Log response headers for debugging - log::debug!("GAM Response headers:"); - for (name, value) in response.get_headers() { - log::debug!(" {}: {:?}", name, value); - } - - // Handle response body safely - let body_bytes = response.take_body_bytes(); - let body = match std::str::from_utf8(&body_bytes) { - Ok(body_str) => body_str.to_string(), - Err(e) => { - log::warn!("Could not read response body as UTF-8: {:?}", e); - - // Try to decompress if it's Brotli compressed - let mut decompressed = Vec::new(); - match brotli::BrotliDecompress( - &mut std::io::Cursor::new(&body_bytes), - &mut decompressed, - ) { - Ok(_) => match std::str::from_utf8(&decompressed) { - Ok(decompressed_str) => { - log::debug!( - "Successfully decompressed Brotli response: {} bytes", - decompressed_str.len() - ); - decompressed_str.to_string() - } - Err(e2) => { - log::warn!("Could not read decompressed body as UTF-8: {:?}", e2); - format!("{{\"error\": \"decompression_failed\", \"message\": \"Could not decode decompressed response\", \"original_error\": \"{:?}\"}}", e2) - } - }, - Err(e2) => { - log::warn!("Could not decompress Brotli response: {:?}", e2); - // Return a placeholder since we can't parse the binary response - format!("{{\"error\": \"compression_failed\", \"message\": \"Could not decompress response\", \"original_error\": \"{:?}\"}}", e2) - } - } - } - }; - - log::debug!("GAM Response body length: {} bytes", body.len()); - - Ok(Response::from_status(response.get_status()) - .with_header(header::CONTENT_TYPE, "application/json") - .with_header(header::CACHE_CONTROL, "no-store, private") - .with_header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .with_header("X-GAM-Test", "true") - .with_header("X-Custom-URL", "true") - .with_header("x-compress-hint", "on") - .with_body_json(&json!({ - "status": "custom_url_test", - "original_url": custom_url, - "response_status": response.get_status().as_u16(), - "response_body": body, - "message": "Custom URL test completed" - })) - .map_err(|e| { - Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize response: {}", e), - }) - })?) - } - Err(e) => { - log::error!("Error sending custom GAM request: {:?}", e); - Response::from_status(StatusCode::INTERNAL_SERVER_ERROR) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "Failed to send custom GAM request", - "details": format!("{:?}", e), - "original_url": custom_url - })) - .map_err(|e| { - Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize error response: {}", e), - }) - }) - } - } -} - -/// Handle GAM response rendering in iframe -pub async fn handle_gam_render( - settings: &Settings, - req: Request, -) -> Result> { - log::info!("Handling GAM response rendering"); - - // Check consent status from cookie - let consent = get_consent_from_request(&req).unwrap_or_default(); - let advertising_consent = consent.advertising; - - if !advertising_consent { - return Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "No advertising consent", - "message": "GAM requests require advertising consent" - })) - .map_err(|e| { - Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize response: {}", e), - }) - }); - } - - // Create GAM request and get response - let gam_req = match GamRequest::new(settings, &req) { - Ok(req) => req.with_prmtvctx("129627,137412,138272,139095,139096,139218,141364,143196,143210,143211,143214,143217,144331,144409,144438,144444,144488,144543,144663,144679,144731,144824,144916,145933,146347,146348,146349,146350,146351,146370,146383,146391,146392,146393,146424,146995,147077,147740,148616,148627,148628,149007,150420,150663,150689,150690,150692,150752,150753,150755,150756,150757,150764,150770,150781,150862,154609,155106,155109,156204,164183,164573,165512,166017,166019,166484,166486,166487,166488,166492,166494,166495,166497,166511,167639,172203,172544,173548,176066,178053,178118,178120,178121,178133,180321,186069,199642,199691,202074,202075,202081,233782,238158,adv,bhgp,bhlp,bhgw,bhlq,bhlt,bhgx,bhgv,bhgu,bhhb,rts".to_string()), - Err(e) => { - return Response::from_status(StatusCode::INTERNAL_SERVER_ERROR) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "Failed to create GAM request", - "details": format!("{:?}", e) - })) - .map_err(|e| Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize error response: {}", e), - })); - } - }; - - // Get GAM response - let gam_response = match gam_req.send_request(settings).await { - Ok(response) => response, - Err(e) => { - return Response::from_status(StatusCode::INTERNAL_SERVER_ERROR) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "Failed to get GAM response", - "details": format!("{:?}", e) - })) - .map_err(|e| { - Report::new(TrustedServerError::Gam { - message: format!("Failed to serialize error response: {}", e), - }) - }); - } - }; - - // Parse the GAM response to extract HTML - let response_body = gam_response.into_body_str(); - log::info!("Parsing GAM response for HTML extraction"); - - // The GAM response format is: {"/ad_unit_path":["html",0,null,null,0,90,728,0,0,null,null,null,null,null,[...],null,null,null,null,null,null,null,0,null,null,null,null,null,null,"creative_id","line_item_id"],"..."} - // We need to extract the HTML part after the JSON array - - let html_content = if response_body.contains("") { - // Find the start of HTML content - if let Some(html_start) = response_body.find("") { - let html = &response_body[html_start..]; - log::debug!("Extracted HTML content: {} bytes", html.len()); - html.to_string() - } else { - format!("

Error: Could not find HTML content in GAM response

{}
", - response_body.chars().take(500).collect::()) - } - } else { - // Fallback: return the raw response in a safe HTML wrapper - format!( - "

GAM Response (no HTML found):

{}
", - response_body.chars().take(1000).collect::() - ) - }; - - // Create a safe HTML page that renders the ad content in an iframe - let render_page = format!( - r#" - - - - - GAM Ad Render Test - - - -
-
-

🎯 GAM Ad Render Test

-

Rendering Google Ad Manager response in iframe

-
- -
- Status: Ad content loaded successfully
- Response Size: {} bytes
- Timestamp: {} -
- -
- - - -
- - - - -
- - - -"#, - html_content.len(), - chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"), - html_content.replace("\"", """).replace("'", "'"), - html_content.len(), - html_content.chars().take(200).collect::() - ); - - Ok(Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "text/html; charset=utf-8") - .with_header(header::CACHE_CONTROL, "no-store, private") - .with_header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .with_header("X-GAM-Render", "true") - .with_header("X-Synthetic-ID", &gam_req.synthetic_id) - .with_header("X-Correlator", &gam_req.correlator) - .with_body(render_page)) -} - -/// Check if the path is for a GAM asset -pub fn is_gam_asset_path(path: &str) -> bool { - // Common GAM paths that we know about - path.contains("/tag/js/") || // Google Tag Manager/GAM scripts - path.contains("/pagead/") || // GAM ad serving and interactions - path.contains("/gtag/js") || // Google Analytics/GAM gtag scripts - path.contains("/gampad/") || // GAM ad requests (gampad/ads) - path.contains("/bg/") || // GAM background scripts - path.contains("/sodar") || // GAM traffic quality checks - path.contains("/getconfig/") || // GAM configuration requests - path.contains("/generate_204") || // GAM tracking pixels - path.contains("/recaptcha/") || // reCAPTCHA requests - path.contains("/static/topics/") || // GAM topics framework - path.contains("safeframe") // GAM safe frame containers -} - -/// Rewrite hardcoded URLs in GAM JavaScript to use first-party proxy -pub fn rewrite_gam_urls(content: &str) -> String { - log::info!("Starting GAM URL rewriting..."); - - // Define the URL mappings based on the user's configuration - let url_mappings = [ - // Primary GAM domains - ("securepubads.g.doubleclick.net", "edgepubs.com"), - ("googletagservices.com", "edgepubs.com"), - ("googlesyndication.com", "edgepubs.com"), - ("pagead2.googlesyndication.com", "edgepubs.com"), - ("tpc.googlesyndication.com", "edgepubs.com"), - // GAM-specific subdomains that might appear - ("www.googletagservices.com", "edgepubs.com"), - ("www.googlesyndication.com", "edgepubs.com"), - ("static.googleadsserving.cn", "edgepubs.com"), - // Ad serving domains - ("doubleclick.net", "edgepubs.com"), - ("www.google.com/adsense", "edgepubs.com/adsense"), - // Google ad quality and traffic domains - ("adtrafficquality.google", "edgepubs.com"), - ("ep1.adtrafficquality.google", "edgepubs.com"), - ("ep2.adtrafficquality.google", "edgepubs.com"), - ("ep3.adtrafficquality.google", "edgepubs.com"), - // Other Google ad-related domains - ( - "6ab9b2c571ea5e8cf287325e9ebeaa41.safeframe.googlesyndication.com", - "edgepubs.com", - ), - ("www.google.com/recaptcha", "edgepubs.com/recaptcha"), - ]; - - let mut rewritten_content = content.to_string(); - let mut total_replacements = 0; - - for (original_domain, proxy_domain) in &url_mappings { - // Count replacements for this domain - let before_count = rewritten_content.matches(original_domain).count(); - - if before_count > 0 { - log::info!( - "Found {} occurrences of '{}' to rewrite", - before_count, - original_domain - ); - - // Replace both HTTP and HTTPS versions - rewritten_content = rewritten_content.replace( - &format!("https://{}", original_domain), - &format!("https://{}", proxy_domain), - ); - rewritten_content = rewritten_content.replace( - &format!("http://{}", original_domain), - &format!("https://{}", proxy_domain), - ); - - // Also replace protocol-relative URLs (//domain.com) - rewritten_content = rewritten_content.replace( - &format!("//{}", original_domain), - &format!("//{}", proxy_domain), - ); - - // Replace domain-only references (for cases where protocol is added separately) - rewritten_content = rewritten_content.replace( - &format!("\"{}\"", original_domain), - &format!("\"{}\"", proxy_domain), - ); - rewritten_content = rewritten_content.replace( - &format!("'{}'", original_domain), - &format!("'{}'", proxy_domain), - ); - - let after_count = rewritten_content.matches(original_domain).count(); - let replacements = before_count - after_count; - total_replacements += replacements; - - if replacements > 0 { - log::info!( - "Replaced {} occurrences of '{}' with '{}'", - replacements, - original_domain, - proxy_domain - ); - } - } - } - - log::info!( - "GAM URL rewriting complete. Total replacements: {}", - total_replacements - ); - - // Log a sample of the rewritten content for debugging (first 500 chars) - if total_replacements > 0 { - let sample_length = std::cmp::min(500, rewritten_content.len()); - log::debug!( - "Rewritten content sample: {}", - &rewritten_content[..sample_length] - ); - } - - rewritten_content -} - -/// Handle GAM asset serving (JavaScript files and other resources) -pub async fn handle_gam_asset( - _settings: &Settings, - req: Request, -) -> Result> { - let path = req.get_path(); - log::info!("Handling GAM asset request: {}", path); - - // Log request details for debugging - log::info!("GAM Asset Request Details:"); - log::info!(" - Path: {}", path); - log::info!(" - Method: {}", req.get_method()); - log::info!(" - Full URL: {}", req.get_url()); - - // Determine backend and target path - let (backend_name, original_host, target_path) = if path.contains("/tag/js/test.js") { - // Special case: our renamed test.js should map to the original gpt.js - ( - "gam_backend", - "securepubads.g.doubleclick.net", - "/tag/js/gpt.js".to_string(), - ) - } else if path.contains("/pagead/") && path.contains("googlesyndication") { - ( - "pagead2_googlesyndication_backend", - "pagead2.googlesyndication.com", - path.to_string(), - ) - } else { - // Default: all other GAM requests go to main GAM backend - ( - "gam_backend", - "securepubads.g.doubleclick.net", - path.to_string(), - ) - }; - - log::info!( - "Serving GAM asset from backend: {} (original host: {})", - backend_name, - original_host - ); - - // Construct full URL - let mut full_url = format!("https://{}{}", original_host, target_path); - if let Some(query) = req.get_url().query() { - full_url.push('?'); - full_url.push_str(query); - } - - // Special handling for /gampad/ads requests - if target_path.contains("/gampad/ads") { - log::info!("Applying URL parameter rewriting for GAM ad request"); - full_url = full_url.replace( - "url=https%3A%2F%2Fedgepubs.com%2F", - "url=https%3A%2F%2Fwww.test-publisher.com%2F", - ); - } - - let mut asset_req = Request::new(req.get_method().clone(), &full_url); - - // Copy headers from original request - for (name, value) in req.get_headers() { - asset_req.set_header(name, value); - } - asset_req.set_header(header::HOST, original_host); - - // Send to backend - match asset_req.send(backend_name) { - Ok(mut response) => { - log::info!( - "Received GAM asset response: status={}", - response.get_status() - ); - - // Check if JavaScript content needs rewriting - let content_type = response - .get_header(header::CONTENT_TYPE) - .and_then(|h| h.to_str().ok()) - .unwrap_or(""); - - let needs_rewriting = content_type.contains("javascript") || path.contains(".js"); - - if needs_rewriting { - // Handle content-disposition header - let original_content_disposition = response - .get_header("content-disposition") - .and_then(|h| h.to_str().ok()) - .map(|s| s.to_string()); - - response.remove_header("content-disposition"); - - // Get response body - let body_bytes = response.take_body_bytes(); - let original_length = body_bytes.len(); - - // Handle Brotli compression if present - let decompressed_body = if response - .get_header("content-encoding") - .and_then(|h| h.to_str().ok()) - == Some("br") - { - log::info!("Detected Brotli compression, decompressing..."); - let mut decompressed = Vec::new(); - match brotli::Decompressor::new(&body_bytes[..], 4096) - .read_to_end(&mut decompressed) - { - Ok(_) => { - log::info!("Successfully decompressed {} bytes", original_length); - decompressed - } - Err(e) => { - log::error!("Failed to decompress Brotli data: {:?}", e); - return Err(Report::new(TrustedServerError::Gam { - message: format!("Failed to decompress GAM asset: {}", e), - })); - } - } - } else { - body_bytes - }; - - // Convert to string - let body = match std::str::from_utf8(&decompressed_body) { - Ok(body_str) => body_str.to_string(), - Err(e) => { - log::error!("Invalid UTF-8 in GAM asset: {:?}", e); - return Err(Report::new(TrustedServerError::InvalidUtf8 { - message: format!("Invalid UTF-8 in GAM asset: {}", e), - })); - } - }; - - // Rewrite URLs - let rewritten_body = rewrite_gam_urls(&body); - let rewritten_length = rewritten_body.len(); - - log::info!( - "Rewritten GAM JavaScript: {} -> {} bytes", - original_length, - rewritten_length - ); - - // Create new response - let mut new_response = Response::from_status(response.get_status()); - - // Copy headers except problematic ones - for (name, value) in response.get_headers() { - let header_name = name.as_str().to_lowercase(); - if header_name == "content-disposition" - || header_name == "content-encoding" - || header_name == "content-length" - { - continue; - } - new_response.set_header(name, value); - } - - // Set body and headers - new_response.set_body(rewritten_body); - new_response - .set_header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate"); - new_response.set_header("X-Content-Rewritten", "true"); - - // Restore content-disposition if it existed - if let Some(original_disposition) = original_content_disposition { - new_response.set_header("content-disposition", &original_disposition); - } - - Ok(new_response) - } else { - // No rewriting needed, serve as-is - response.set_header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate"); - Ok(response) - } - } - Err(e) => { - log::error!("Error fetching GAM asset from {}: {:?}", backend_name, e); - Err(Report::new(TrustedServerError::Gam { - message: format!( - "Failed to fetch GAM asset from {} for path {}: {}", - backend_name, path, e - ), - })) - } - } -} - -pub fn handle_gam_test_page( - _settings: &Settings, - _req: Request, -) -> Result> { - Ok(Response::from_status(StatusCode::OK) - .with_body(GAM_TEST_TEMPLATE) - .with_header(header::CONTENT_TYPE, "text/html") - .with_header("x-compress-hint", "on")) -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - use crate::test_support::tests::create_test_settings; - - fn create_test_request() -> Request { - let mut req = Request::new(Method::GET, "https://example.com/test"); - req.set_header(header::USER_AGENT, "Mozilla/5.0 Test Browser"); - req.set_header("X-Synthetic-Trusted-Server", "test-synthetic-id-123"); - req - } - - #[test] - fn test_gam_request_new() { - let settings = create_test_settings(); - let req = create_test_request(); - - let gam_req = GamRequest::new(&settings, &req).unwrap(); - - assert_eq!(gam_req.publisher_id, "21796327522"); - assert_eq!(gam_req.ad_units.len(), 2); - assert_eq!(gam_req.ad_units[0], "test_unit_1"); - assert_eq!(gam_req.ad_units[1], "test_unit_2"); - assert_eq!(gam_req.page_url, "https://example.com/test"); - assert_eq!(gam_req.user_agent, "Mozilla/5.0 Test Browser"); - assert_eq!(gam_req.synthetic_id, "test-synthetic-id-123"); - assert!(gam_req.prmtvctx.is_none()); - assert!(!gam_req.correlator.is_empty()); - } - - #[test] - fn test_gam_request_with_missing_headers() { - let settings = create_test_settings(); - let req = Request::new(Method::GET, "https://example.com/test"); - - let gam_req = GamRequest::new(&settings, &req).unwrap(); - - assert_eq!( - gam_req.user_agent, - "Mozilla/5.0 (compatible; TrustedServer/1.0)" - ); - assert_eq!(gam_req.synthetic_id, "unknown"); - } - - #[test] - fn test_gam_request_with_prmtvctx() { - let settings = create_test_settings(); - let req = create_test_request(); - - let gam_req = GamRequest::new(&settings, &req) - .unwrap() - .with_prmtvctx("test_context_123".to_string()); - - assert_eq!(gam_req.prmtvctx, Some("test_context_123".to_string())); - } - - #[test] - fn test_build_golden_url() { - let settings = create_test_settings(); - let req = create_test_request(); - - let gam_req = GamRequest::new(&settings, &req) - .unwrap() - .with_prmtvctx("test_permutive_context".to_string()); - - let url = gam_req.build_golden_url(); - - assert!(url.starts_with("https://securepubads.g.doubleclick.net/gampad/ads?")); - assert!(url.contains("correlator=")); - assert!(url.contains("iu_parts=21796327522%2Ctrustedserver%2Chomepage")); - assert!(url.contains("url=https%3A%2F%2Fexample.com%2Ftest")); - assert!(url.contains( - "cust_params=permutive%3Dtest_permutive_context%26puid%3Dtest-synthetic-id-123" - )); - assert!(url.contains("output=ldjh")); - assert!(url.contains("gdfp_req=1")); - } - - #[test] - fn test_build_golden_url_without_prmtvctx() { - let settings = create_test_settings(); - let req = create_test_request(); - - let gam_req = GamRequest::new(&settings, &req).unwrap(); - let url = gam_req.build_golden_url(); - - assert!(!url.contains("cust_params=")); - assert!(!url.contains("permutive=")); - } - - #[test] - fn test_url_encoding_in_build_golden_url() { - let settings = create_test_settings(); - let mut req = Request::new( - Method::GET, - "https://example.com/test?param=value&special=test%20space", - ); - req.set_header("X-Synthetic-Trusted-Server", "test-id"); - - let gam_req = GamRequest::new(&settings, &req).unwrap(); - let url = gam_req.build_golden_url(); - - // Check that URL parameters are properly encoded - assert!(url.contains( - "url=https%3A%2F%2Fexample.com%2Ftest%3Fparam%3Dvalue%26special%3Dtest%2520space" - )); - } - - #[test] - fn test_correlator_uniqueness() { - let settings = create_test_settings(); - let req = create_test_request(); - - let gam_req1 = GamRequest::new(&settings, &req).unwrap(); - let gam_req2 = GamRequest::new(&settings, &req).unwrap(); - - // Correlators should be unique for each request - assert_ne!(gam_req1.correlator, gam_req2.correlator); - } - - // Integration tests for GAM handlers - #[tokio::test] - async fn test_handle_gam_test_without_consent() { - let settings = create_test_settings(); - let req = Request::new(Method::GET, "https://example.com/gam-test"); - - let response = handle_gam_test(&settings, req).await.unwrap(); - - assert_eq!(response.get_status(), StatusCode::OK); - let body = response.into_body_str(); - let json: serde_json::Value = serde_json::from_str(&body).unwrap(); - - assert_eq!(json["error"], "No advertising consent"); - assert_eq!(json["message"], "GAM requests require advertising consent"); - } - - #[tokio::test] - async fn test_handle_gam_test_with_header_consent() { - let settings = create_test_settings(); - let mut req = Request::new(Method::GET, "https://example.com/gam-test"); - req.set_header("X-Consent-Advertising", "true"); - req.set_header("X-Synthetic-Trusted-Server", "test-synthetic-id"); - - // Note: This test will fail when actually sending to GAM backend - // In a real test environment, we'd mock the backend response - let response = handle_gam_test(&settings, req).await; - assert!(response.is_ok() || response.is_err()); // Test runs either way - } - - #[tokio::test] - async fn test_handle_gam_golden_url() { - let settings = create_test_settings(); - let req = Request::new(Method::GET, "https://example.com/gam-golden-url"); - - let response = handle_gam_golden_url(&settings, req).await.unwrap(); - - assert_eq!(response.get_status(), StatusCode::OK); - let body = response.into_body_str(); - let json: serde_json::Value = serde_json::from_str(&body).unwrap(); - - assert_eq!(json["status"], "golden_url_replay"); - assert_eq!(json["message"], "Ready for captured URL testing"); - assert!(json["next_steps"].is_array()); - } - - #[tokio::test] - async fn test_handle_gam_custom_url_without_consent() { - let settings = create_test_settings(); - let mut req = Request::new(Method::POST, "https://example.com/gam-test-custom-url"); - req.set_body( - json!({ - "url": "https://securepubads.g.doubleclick.net/gampad/ads?test=1" - }) - .to_string(), - ); - - let response = handle_gam_custom_url(&settings, req).await.unwrap(); - - assert_eq!(response.get_status(), StatusCode::OK); - let body = response.into_body_str(); - let json: serde_json::Value = serde_json::from_str(&body).unwrap(); - - assert_eq!(json["error"], "No advertising consent"); - } - - #[tokio::test] - async fn test_handle_gam_custom_url_with_invalid_body() { - let settings = create_test_settings(); - let mut req = Request::new(Method::POST, "https://example.com/gam-test-custom-url"); - req.set_header("X-Consent-Advertising", "true"); - req.set_body("invalid json"); - - let response = handle_gam_custom_url(&settings, req).await; - - // Should return an error for invalid JSON - assert!(response.is_err()); - } - - #[tokio::test] - async fn test_handle_gam_custom_url_missing_url_field() { - let settings = create_test_settings(); - let mut req = Request::new(Method::POST, "https://example.com/gam-test-custom-url"); - req.set_header("X-Consent-Advertising", "true"); - req.set_body( - json!({ - "other_field": "value" - }) - .to_string(), - ); - - let response = handle_gam_custom_url(&settings, req).await; - - // Should return an error for missing URL field - assert!(response.is_err()); - } - - #[tokio::test] - async fn test_handle_gam_render_without_consent() { - let settings = create_test_settings(); - let req = Request::new(Method::GET, "https://example.com/gam-render"); - - let response = handle_gam_render(&settings, req).await.unwrap(); - - assert_eq!(response.get_status(), StatusCode::OK); - let body = response.into_body_str(); - let json: serde_json::Value = serde_json::from_str(&body).unwrap(); - - assert_eq!(json["error"], "No advertising consent"); - assert_eq!(json["message"], "GAM requests require advertising consent"); - } - - // Tests for is_gam_asset_path and rewrite_gam_urls functions - #[test] - fn test_is_gam_asset_path() { - // Test positive cases - assert!(is_gam_asset_path("/tag/js/gpt.js")); - assert!(is_gam_asset_path("/pagead/ads")); - assert!(is_gam_asset_path("/gampad/ads?params=test")); - assert!(is_gam_asset_path("/recaptcha/api.js")); - assert!(is_gam_asset_path("/safeframe/1-0-40/html/container.html")); - - // Test negative cases - assert!(!is_gam_asset_path("/")); - assert!(!is_gam_asset_path("/index.html")); - assert!(!is_gam_asset_path("/api/v1/data")); - } - - #[test] - fn test_rewrite_gam_urls() { - // Test basic domain replacement - let content = "https://securepubads.g.doubleclick.net/tag/js/gpt.js"; - let rewritten = rewrite_gam_urls(content); - assert_eq!(rewritten, "https://edgepubs.com/tag/js/gpt.js"); - - // Test multiple domains and protocols - let content = r#" - var url1 = "https://googletagservices.com/tag/js/gpt.js"; - //securepubads.g.doubleclick.net/tag/js/gpt.js - https://tpc.googlesyndication.com/simgad/123456?param=value#anchor - "#; - let rewritten = rewrite_gam_urls(content); - assert!(rewritten.contains("https://edgepubs.com/tag/js/gpt.js")); - assert!(rewritten.contains("//edgepubs.com/tag/js/gpt.js")); - assert!(rewritten.contains("https://edgepubs.com/simgad/123456?param=value#anchor")); - assert!(!rewritten.contains("googletagservices.com")); - assert!(!rewritten.contains("googlesyndication.com")); - - // Test special domains (adtrafficquality, safeframe, recaptcha) - let content = r#" - https://ep1.adtrafficquality.google/beacon - https://6ab9b2c571ea5e8cf287325e9ebeaa41.safeframe.googlesyndication.com/safeframe/1-0-40/html/container.html - https://www.google.com/recaptcha/api.js - "#; - let rewritten = rewrite_gam_urls(content); - assert!(rewritten.contains("https://edgepubs.com/beacon")); - assert!(rewritten.contains("https://edgepubs.com/safeframe/1-0-40/html/container.html")); - assert!(rewritten.contains("https://edgepubs.com/recaptcha/api.js")); - - // Test content that should not be changed - let content = "https://example.com/some/path"; - let rewritten = rewrite_gam_urls(content); - assert_eq!(content, rewritten); - } - - #[test] - fn test_rewrite_gam_urls_edge_cases() { - // Test empty content - assert_eq!(rewrite_gam_urls(""), ""); - - // Test protocol-relative URLs - let content = "//securepubads.g.doubleclick.net/tag/js/gpt.js"; - let rewritten = rewrite_gam_urls(content); - assert_eq!(rewritten, "//edgepubs.com/tag/js/gpt.js"); - - // Test case sensitivity (should not replace) - let content = "https://SECUREPUBADS.G.DOUBLECLICK.NET/tag/js/gpt.js"; - let rewritten = rewrite_gam_urls(content); - assert!(rewritten.contains("SECUREPUBADS.G.DOUBLECLICK.NET")); - - // Test URLs in HTML attributes - let content = - r#""#; - let rewritten = rewrite_gam_urls(content); - assert!(rewritten.contains(r#"src="https://edgepubs.com/tag/js/gpt.js""#)); - } -} diff --git a/crates/common/src/gdpr.rs b/crates/common/src/gdpr.rs deleted file mode 100644 index 4a72653..0000000 --- a/crates/common/src/gdpr.rs +++ /dev/null @@ -1,488 +0,0 @@ -//! GDPR consent management and compliance. -//! -//! This module provides functionality for managing GDPR consent, including -//! consent tracking, data subject requests, and compliance with EU privacy regulations. - -use error_stack::{Report, ResultExt}; -use fastly::http::{header, Method, StatusCode}; -use fastly::{Request, Response}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use crate::constants::HEADER_X_SUBJECT_ID; -use crate::cookies; -use crate::error::TrustedServerError; -use crate::settings::Settings; - -/// GDPR consent information for a user. -/// -/// Tracks consent status for different purposes as required by GDPR. -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GdprConsent { - /// Consent for analytics and measurement. - pub analytics: bool, - /// Consent for personalized advertising. - pub advertising: bool, - /// Consent for functional cookies and features. - pub functional: bool, - /// Unix timestamp when consent was given. - pub timestamp: i64, - /// Version of the consent framework. - pub version: String, -} - -/// User data collected for GDPR compliance. -/// -/// Contains all data collected about a user that must be made available -/// for data subject access requests. -#[derive(Debug, Serialize, Deserialize)] -pub struct UserData { - /// Number of visits by the user. - pub visit_count: i32, - /// Unix timestamp of the last visit. - pub last_visit: i64, - /// List of ad interaction events. - pub ad_interactions: Vec, - /// History of consent changes. - pub consent_history: Vec, -} - -impl Default for GdprConsent { - fn default() -> Self { - Self { - analytics: false, - advertising: false, - functional: false, - timestamp: chrono::Utc::now().timestamp(), - version: "1.0".to_string(), - } - } -} - -impl Default for UserData { - fn default() -> Self { - Self { - visit_count: 0, - last_visit: chrono::Utc::now().timestamp(), - ad_interactions: Vec::new(), - consent_history: Vec::new(), - } - } -} - -/// Extracts GDPR consent information from a request. -/// -/// Looks for consent information in the `gdpr_consent` cookie and parses -/// it into a [`GdprConsent`] structure. -/// -/// Returns [`None`] if no consent cookie is found or parsing fails. -pub fn get_consent_from_request(req: &Request) -> Option { - match cookies::handle_request_cookies(req) { - Ok(Some(jar)) => { - if let Some(consent_cookie) = jar.get("gdpr_consent") { - if let Ok(consent) = serde_json::from_str(consent_cookie.value()) { - return Some(consent); - } - } - None - } - Ok(None) => None, - Err(e) => { - log::warn!("Failed to parse cookies for consent: {:?}", e); - None - } - } -} - -/// Creates a GDPR consent cookie string. -/// -/// Generates a properly formatted cookie string with the consent data, -/// including security attributes and domain settings. -pub fn create_consent_cookie(settings: &Settings, consent: &GdprConsent) -> String { - format!( - "gdpr_consent={}; Domain={}; Path=/; Secure; SameSite=Lax; Max-Age=31536000", - serde_json::to_string(consent).unwrap_or_default(), - settings.publisher.cookie_domain, - ) -} - -/// Handles GDPR consent management requests. -/// -/// Processes GET and POST requests to the `/gdpr/consent` endpoint: -/// - GET: Returns current consent status -/// - POST: Updates consent preferences -/// -/// # Errors -/// -/// Returns a [`TrustedServerError`] if: -/// - JSON serialization/deserialization fails -/// - Response creation fails -pub fn handle_consent_request( - settings: &Settings, - req: Request, -) -> Result> { - match *req.get_method() { - Method::GET => { - // Return current consent status - let consent = get_consent_from_request(&req).unwrap_or_default(); - let json_body = serde_json::to_string(&consent).change_context( - TrustedServerError::GdprConsent { - message: "Failed to serialize consent data".to_string(), - }, - )?; - - Ok(Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body(json_body)) - } - Method::POST => { - // Update consent preferences - let consent: GdprConsent = serde_json::from_slice(req.into_body_bytes().as_slice()) - .change_context(TrustedServerError::GdprConsent { - message: "Failed to parse consent request body".to_string(), - })?; - - let json_body = serde_json::to_string(&consent).change_context( - TrustedServerError::GdprConsent { - message: "Failed to serialize consent response".to_string(), - }, - )?; - - let mut response = Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body(json_body); - - response.set_header( - header::SET_COOKIE, - create_consent_cookie(settings, &consent), - ); - Ok(response) - } - _ => { - Ok(Response::from_status(StatusCode::METHOD_NOT_ALLOWED) - .with_body("Method not allowed")) - } - } -} - -/// Handles GDPR data subject access requests. -/// -/// Processes requests to view or delete user data as required by GDPR: -/// - GET: Returns all collected user data -/// - DELETE: Removes all user data -/// -/// Requires the `X-Subject-ID` header for authentication. -/// -/// # Errors -/// -/// Returns a [`TrustedServerError`] if: -/// - Header value extraction fails -/// - JSON serialization fails -pub fn handle_data_subject_request( - _settings: &Settings, - req: Request, -) -> Result> { - match *req.get_method() { - Method::GET => { - // Handle data access request - if let Some(synthetic_id) = req.get_header(HEADER_X_SUBJECT_ID) { - // Create a HashMap to store all user-related data - let mut data: HashMap = HashMap::new(); - - // TODO: Implement actual data retrieval from KV store - // For now, return empty user data - let id_str = synthetic_id.to_str().change_context( - TrustedServerError::InvalidHeaderValue { - message: "Invalid subject ID header value".to_string(), - }, - )?; - data.insert(id_str.to_string(), UserData::default()); - - let json_body = serde_json::to_string(&data).change_context( - TrustedServerError::GdprConsent { - message: "Failed to serialize user data".to_string(), - }, - )?; - - Ok(Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body(json_body)) - } else { - Ok(Response::from_status(StatusCode::BAD_REQUEST).with_body("Missing subject ID")) - } - } - Method::DELETE => { - // Handle right to erasure (right to be forgotten) - if let Some(_synthetic_id) = req.get_header(HEADER_X_SUBJECT_ID) { - // TODO: Implement data deletion from KV store - Ok(Response::from_status(StatusCode::OK) - .with_body("Data deletion request processed")) - } else { - Ok(Response::from_status(StatusCode::BAD_REQUEST).with_body("Missing subject ID")) - } - } - _ => { - Ok(Response::from_status(StatusCode::METHOD_NOT_ALLOWED) - .with_body("Method not allowed")) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fastly::{Body, Request}; - - use crate::test_support::tests::create_test_settings; - - #[test] - fn test_gdpr_consent_default() { - let consent = GdprConsent::default(); - assert!(!consent.analytics); - assert!(!consent.advertising); - assert!(!consent.functional); - assert_eq!(consent.version, "1.0"); - assert!(consent.timestamp > 0); - } - - #[test] - fn test_user_data_default() { - let data = UserData::default(); - assert_eq!(data.visit_count, 0); - assert!(data.last_visit > 0); - assert!(data.ad_interactions.is_empty()); - assert!(data.consent_history.is_empty()); - } - - #[test] - fn test_gdpr_consent_serialization() { - let consent = GdprConsent { - analytics: true, - advertising: false, - functional: true, - timestamp: 1234567890, - version: "2.0".to_string(), - }; - - let json = serde_json::to_string(&consent).unwrap(); - assert!(json.contains("\"analytics\":true")); - assert!(json.contains("\"advertising\":false")); - assert!(json.contains("\"functional\":true")); - assert!(json.contains("\"timestamp\":1234567890")); - assert!(json.contains("\"version\":\"2.0\"")); - - let deserialized: GdprConsent = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.analytics, consent.analytics); - assert_eq!(deserialized.advertising, consent.advertising); - assert_eq!(deserialized.functional, consent.functional); - assert_eq!(deserialized.timestamp, consent.timestamp); - assert_eq!(deserialized.version, consent.version); - } - - #[test] - fn test_create_consent_cookie() { - let settings = create_test_settings(); - let consent = GdprConsent { - analytics: true, - advertising: true, - functional: true, - timestamp: 1234567890, - version: "1.0".to_string(), - }; - - let cookie = create_consent_cookie(&settings, &consent); - assert!(cookie.starts_with("gdpr_consent=")); - assert!(cookie.contains(format!("Domain={}", settings.publisher.cookie_domain).as_str())); - assert!(cookie.contains("Path=/")); - assert!(cookie.contains("Secure")); - assert!(cookie.contains("SameSite=Lax")); - assert!(cookie.contains("Max-Age=31536000")); - } - - #[test] - fn test_get_consent_from_request_no_cookie() { - let req = Request::get("https://example.com"); - let consent = get_consent_from_request(&req); - assert!(consent.is_none()); - } - - #[test] - fn test_get_consent_from_request_with_valid_cookie() { - let mut req = Request::get("https://example.com"); - let consent_data = GdprConsent { - analytics: true, - advertising: false, - functional: true, - timestamp: 1234567890, - version: "1.0".to_string(), - }; - let cookie_value = format!( - "gdpr_consent={}", - serde_json::to_string(&consent_data).unwrap() - ); - req.set_header(header::COOKIE, cookie_value); - - let consent = get_consent_from_request(&req); - assert!(consent.is_some()); - let consent = consent.unwrap(); - assert!(consent.analytics); - assert!(!consent.advertising); - assert!(consent.functional); - } - - #[test] - fn test_get_consent_from_request_with_invalid_cookie() { - let mut req = Request::get("https://example.com"); - req.set_header(header::COOKIE, "gdpr_consent=invalid-json"); - - let consent = get_consent_from_request(&req); - assert!(consent.is_none()); - } - - #[test] - fn test_handle_consent_request_get() { - let settings = create_test_settings(); - let req = Request::get("https://example.com/gdpr/consent"); - - let response = handle_consent_request(&settings, req).unwrap(); - assert_eq!(response.get_status(), StatusCode::OK); - assert_eq!( - response.get_header_str(header::CONTENT_TYPE), - Some("application/json") - ); - - let body = response.into_body_str(); - let consent: GdprConsent = serde_json::from_str(&body).unwrap(); - assert!(!consent.analytics); // Default values - assert!(!consent.advertising); - assert!(!consent.functional); - } - - #[test] - fn test_handle_consent_request_post() { - let settings = create_test_settings(); - let consent_data = GdprConsent { - analytics: true, - advertising: true, - functional: false, - timestamp: 1234567890, - version: "1.0".to_string(), - }; - - let mut req = Request::post("https://example.com/gdpr/consent"); - req.set_body(Body::from(serde_json::to_string(&consent_data).unwrap())); - - let response = handle_consent_request(&settings, req).unwrap(); - assert_eq!(response.get_status(), StatusCode::OK); - assert_eq!( - response.get_header_str(header::CONTENT_TYPE), - Some("application/json") - ); - - // Check Set-Cookie header - let set_cookie = response.get_header_str(header::SET_COOKIE); - assert!(set_cookie.is_some()); - assert!(set_cookie.unwrap().contains("gdpr_consent=")); - - assert!(set_cookie - .unwrap() - .contains(format!("Domain={}", settings.publisher.cookie_domain).as_str())); - - // Check response body - let body = response.into_body_str(); - let returned_consent: GdprConsent = serde_json::from_str(&body).unwrap(); - assert!(returned_consent.analytics); - assert!(returned_consent.advertising); - assert!(!returned_consent.functional); - } - - #[test] - fn test_handle_consent_request_invalid_method() { - let settings = create_test_settings(); - let req = Request::put("https://example.com/gdpr/consent"); - - let response = handle_consent_request(&settings, req).unwrap(); - assert_eq!(response.get_status(), StatusCode::METHOD_NOT_ALLOWED); - assert_eq!(response.into_body_str(), "Method not allowed"); - } - - #[test] - fn test_handle_data_subject_request_get_with_id() { - let settings = create_test_settings(); - let mut req = Request::get("https://example.com/gdpr/data"); - req.set_header(HEADER_X_SUBJECT_ID, "test-subject-123"); - - let response = handle_data_subject_request(&settings, req).unwrap(); - assert_eq!(response.get_status(), StatusCode::OK); - assert_eq!( - response.get_header_str(header::CONTENT_TYPE), - Some("application/json") - ); - - let body = response.into_body_str(); - let data: HashMap = serde_json::from_str(&body).unwrap(); - assert!(data.contains_key("test-subject-123")); - assert_eq!(data["test-subject-123"].visit_count, 0); // Default value - } - - #[test] - fn test_handle_data_subject_request_get_without_id() { - let settings = create_test_settings(); - let req = Request::get("https://example.com/gdpr/data"); - - let response = handle_data_subject_request(&settings, req).unwrap(); - assert_eq!(response.get_status(), StatusCode::BAD_REQUEST); - assert_eq!(response.into_body_str(), "Missing subject ID"); - } - - #[test] - fn test_handle_data_subject_request_delete_with_id() { - let settings = create_test_settings(); - let mut req = Request::delete("https://example.com/gdpr/data"); - req.set_header(HEADER_X_SUBJECT_ID, "test-subject-123"); - - let response = handle_data_subject_request(&settings, req).unwrap(); - assert_eq!(response.get_status(), StatusCode::OK); - assert_eq!(response.into_body_str(), "Data deletion request processed"); - } - - #[test] - fn test_handle_data_subject_request_delete_without_id() { - let settings = create_test_settings(); - let req = Request::delete("https://example.com/gdpr/data"); - - let response = handle_data_subject_request(&settings, req).unwrap(); - assert_eq!(response.get_status(), StatusCode::BAD_REQUEST); - assert_eq!(response.into_body_str(), "Missing subject ID"); - } - - #[test] - fn test_handle_data_subject_request_invalid_method() { - let settings = create_test_settings(); - let req = Request::post("https://example.com/gdpr/data"); - - let response = handle_data_subject_request(&settings, req).unwrap(); - assert_eq!(response.get_status(), StatusCode::METHOD_NOT_ALLOWED); - assert_eq!(response.into_body_str(), "Method not allowed"); - } - - #[test] - fn test_user_data_serialization() { - let user_data = UserData { - visit_count: 5, - last_visit: 1234567890, - ad_interactions: vec!["click1".to_string(), "view2".to_string()], - consent_history: vec![GdprConsent::default()], - }; - - let json = serde_json::to_string(&user_data).unwrap(); - assert!(json.contains("\"visit_count\":5")); - assert!(json.contains("\"last_visit\":1234567890")); - assert!(json.contains("\"ad_interactions\":[\"click1\",\"view2\"]")); - - let deserialized: UserData = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.visit_count, user_data.visit_count); - assert_eq!(deserialized.last_visit, user_data.last_visit); - assert_eq!(deserialized.ad_interactions.len(), 2); - } -} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 90e5d36..2414a1a 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -23,23 +23,17 @@ //! - [`why`]: Debugging and introspection utilities pub mod ad; -pub mod advertiser; pub mod backend; pub mod constants; pub mod cookies; pub mod creative; pub mod error; -pub mod gam; -pub mod gdpr; pub mod geo; pub mod html_processor; pub mod http_util; pub mod models; pub mod openrtb; -pub mod partners; -pub mod prebid; pub mod prebid_proxy; -pub mod privacy; pub mod proxy; pub mod publisher; pub mod settings; @@ -47,7 +41,5 @@ pub mod settings_data; pub mod streaming_processor; pub mod streaming_replacer; pub mod synthetic; -pub mod templates; pub mod test_support; pub mod tsjs; -pub mod why; diff --git a/crates/common/src/partners.rs b/crates/common/src/partners.rs deleted file mode 100644 index 03782b7..0000000 --- a/crates/common/src/partners.rs +++ /dev/null @@ -1,448 +0,0 @@ -use std::collections::HashMap; - -use error_stack::Report; -use fastly::http::header; -use fastly::{Request, Response}; - -use crate::error::TrustedServerError; -use crate::settings::Settings; - -/// Manages partner-specific URL rewriting and proxy configurations -pub struct PartnerManager { - /// Map of original domain -> proxy domain for rewriting URLs - domain_mappings: HashMap, - /// Map of original domain -> backend name for proxying requests - backend_mappings: HashMap, -} - -impl PartnerManager { - /// Create a new PartnerManager from settings - pub fn from_settings(settings: &Settings) -> Self { - let mut domain_mappings = HashMap::new(); - let mut backend_mappings = HashMap::new(); - - if let Some(partners) = &settings.partners { - // Process GAM partner config - if let Some(gam) = &partners.gam { - if gam.enabled { - for domain in &gam.domains_to_proxy { - domain_mappings.insert(domain.clone(), gam.proxy_domain.clone()); - backend_mappings.insert(domain.clone(), gam.backend_name.clone()); - } - } - } - - // Process Equativ partner config - if let Some(equativ) = &partners.equativ { - if equativ.enabled { - for domain in &equativ.domains_to_proxy { - domain_mappings.insert(domain.clone(), equativ.proxy_domain.clone()); - backend_mappings.insert(domain.clone(), equativ.backend_name.clone()); - } - } - } - - // Process Prebid partner config - if let Some(prebid) = &partners.prebid { - if prebid.enabled { - for domain in &prebid.domains_to_proxy { - domain_mappings.insert(domain.clone(), prebid.proxy_domain.clone()); - backend_mappings.insert(domain.clone(), prebid.backend_name.clone()); - } - } - } - } - - Self { - domain_mappings, - backend_mappings, - } - } - - /// Rewrite a URL to use the configured proxy domain - pub fn rewrite_url(&self, original_url: &str) -> String { - let mut rewritten_url = original_url.to_string(); - - for (original_domain, proxy_domain) in &self.domain_mappings { - if rewritten_url.contains(original_domain) { - rewritten_url = rewritten_url.replace(original_domain, proxy_domain); - // Only replace the first match to avoid multiple replacements - break; - } - } - - rewritten_url - } - - /// Get the backend name for a given domain (for proxying) - pub fn get_backend_for_domain(&self, domain: &str) -> Option<&str> { - self.backend_mappings.get(domain).map(|s| s.as_str()) - } - - /// Check if a domain should be proxied - pub fn should_proxy_domain(&self, domain: &str) -> bool { - self.domain_mappings.contains_key(domain) - } - - /// Get all domains that should be proxied - pub fn get_proxied_domains(&self) -> Vec<&String> { - self.domain_mappings.keys().collect() - } - - /// Rewrite multiple URLs in a text content (for HTML/JS content) - pub fn rewrite_content(&self, content: &str) -> String { - let mut rewritten_content = content.to_string(); - - for (original_domain, proxy_domain) in &self.domain_mappings { - // Use regex-like replacement for all occurrences - rewritten_content = rewritten_content.replace(original_domain, proxy_domain); - } - - rewritten_content - } -} - -/// Handles direct asset serving for partner domains (like auburndao.com). -/// -/// Fetches assets from original partner domains and serves them as first-party content. -/// This bypasses ad blockers and Safari ITP by making all assets appear to come from edgepubs.com. -/// -/// # Errors -/// -/// Returns a [`TrustedServerError`] if asset fetching fails. -pub async fn handle_partner_asset( - _settings: &Settings, - req: Request, -) -> Result> { - let path = req.get_path(); - log::info!("=== HANDLING PARTNER ASSET: {} ===", path); - - // Only handle Equativ/Smart AdServer assets (matching auburndao.com approach) - let (backend_name, original_host) = ("equativ_sascdn_backend", "creatives.sascdn.com"); - - log::info!( - "Serving asset from backend: {} (original host: {})", - backend_name, - original_host - ); - - // Construct full URL using the original host and path - let full_url = format!("https://{}{}", original_host, path); - log::info!("Fetching asset URL: {}", full_url); - - let mut asset_req = Request::new(req.get_method().clone(), &full_url); - - // Copy all headers from original request - for (name, value) in req.get_headers() { - asset_req.set_header(name, value); - } - - // Set the Host header to the original domain for proper routing - asset_req.set_header(header::HOST, original_host); - - // Send to appropriate backend - match asset_req.send(backend_name) { - Ok(mut response) => { - // Match auburndao.com cache control exactly - let cache_control = "max-age=31536000"; - - // No content rewriting needed for Equativ assets (they're mostly images) - // This matches the auburndao.com approach of serving assets directly - - // Match auburndao.com headers exactly - no modifications - response.set_header(header::CACHE_CONTROL, cache_control); - - // Don't modify any other headers - keep them exactly as auburndao.com gets them - - log::debug!("=== ASSET RESPONSE HEADERS FOR {} ===", path); - for (name, value) in response.get_headers() { - log::debug!(" {}: {:?}", name, value); - } - - // No special CORB handling needed for Equativ image assets - - log::info!( - "Partner asset served successfully, cache-control: {}", - cache_control - ); - Ok(response) - } - Err(e) => { - log::error!( - "Error fetching partner asset from {} (original host: {}): {:?}", - backend_name, - original_host, - e - ); - Err(Report::new(TrustedServerError::Gam { - message: format!( - "Failed to fetch partner asset from {} ({}): path={}, error={:?}", - backend_name, original_host, path, e - ), - })) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::settings::{PartnerConfig, Partners, Settings}; - - use crate::test_support::tests::create_test_settings; - - #[test] - fn test_url_rewriting() { - let settings = create_test_settings(); - let manager = PartnerManager::from_settings(&settings); - - // Test GAM URL rewriting - let gam_url = "https://tpc.googlesyndication.com/simgad/12184163379128326694"; - let rewritten = manager.rewrite_url(gam_url); - assert_eq!( - rewritten, - "https://creatives.auburndao.com/simgad/12184163379128326694" - ); - - // Test Equativ URL rewriting - let equativ_url = "https://creatives.sascdn.com/diff/12345/creative.jpg"; - let rewritten = manager.rewrite_url(equativ_url); - assert_eq!( - rewritten, - "https://creatives.auburndao.com/diff/12345/creative.jpg" - ); - - // Test non-matching URL (should remain unchanged) - let other_url = "https://example.com/image.jpg"; - let rewritten = manager.rewrite_url(other_url); - assert_eq!(rewritten, "https://example.com/image.jpg"); - } - - #[test] - fn test_backend_mapping() { - let settings = create_test_settings(); - let manager = PartnerManager::from_settings(&settings); - - assert_eq!( - manager.get_backend_for_domain("tpc.googlesyndication.com"), - Some("gam_proxy_backend") - ); - assert_eq!( - manager.get_backend_for_domain("creatives.sascdn.com"), - Some("equativ_proxy_backend") - ); - assert_eq!(manager.get_backend_for_domain("unknown.domain.com"), None); - } - - #[test] - fn test_content_rewriting() { - let settings = create_test_settings(); - let manager = PartnerManager::from_settings(&settings); - - let html_content = r#" - - - - "#; - - let rewritten = manager.rewrite_content(html_content); - - assert!(rewritten.contains("https://creatives.auburndao.com/simgad/123")); - assert!(rewritten.contains("https://creatives.auburndao.com/gpt/pubads.js")); - assert!(rewritten.contains("https://creatives.auburndao.com/creative.jpg")); - assert!(!rewritten.contains("tpc.googlesyndication.com")); - assert!(!rewritten.contains("securepubads.g.doubleclick.net")); - assert!(!rewritten.contains("creatives.sascdn.com")); - } - - #[test] - fn test_should_proxy_domain() { - let settings = create_test_settings(); - let manager = PartnerManager::from_settings(&settings); - - assert!(manager.should_proxy_domain("tpc.googlesyndication.com")); - assert!(manager.should_proxy_domain("securepubads.g.doubleclick.net")); - assert!(manager.should_proxy_domain("creatives.sascdn.com")); - assert!(!manager.should_proxy_domain("example.com")); - assert!(!manager.should_proxy_domain("unknown.domain.com")); - } - - #[test] - fn test_get_proxied_domains() { - let settings = create_test_settings(); - let manager = PartnerManager::from_settings(&settings); - - let domains = manager.get_proxied_domains(); - assert_eq!(domains.len(), 3); - assert!(domains.iter().any(|d| *d == "tpc.googlesyndication.com")); - assert!(domains - .iter() - .any(|d| *d == "securepubads.g.doubleclick.net")); - assert!(domains.iter().any(|d| *d == "creatives.sascdn.com")); - } - - #[test] - fn test_disabled_partner_config() { - let mut settings = Settings::default(); - - // Create disabled GAM config - let gam_config = PartnerConfig { - enabled: false, - name: "Google Ad Manager".to_string(), - domains_to_proxy: vec!["securepubads.g.doubleclick.net".to_string()], - proxy_domain: "creatives.auburndao.com".to_string(), - backend_name: "gam_proxy_backend".to_string(), - }; - - settings.partners = Some(Partners { - gam: Some(gam_config), - equativ: None, - prebid: None, - }); - - let manager = PartnerManager::from_settings(&settings); - - // Disabled partner should not have any domain mappings - assert!(!manager.should_proxy_domain("securepubads.g.doubleclick.net")); - assert_eq!( - manager.get_backend_for_domain("securepubads.g.doubleclick.net"), - None - ); - - let url = "https://securepubads.g.doubleclick.net/tag/js/gpt.js"; - assert_eq!(manager.rewrite_url(url), url); - } - - #[test] - fn test_empty_partner_config() { - let settings = Settings::default(); - let manager = PartnerManager::from_settings(&settings); - - // No partners configured - assert_eq!(manager.get_proxied_domains().len(), 0); - assert!(!manager.should_proxy_domain("any.domain.com")); - - let url = "https://example.com/test"; - assert_eq!(manager.rewrite_url(url), url); - } - - #[test] - fn test_multiple_replacements_in_single_url() { - let settings = create_test_settings(); - let manager = PartnerManager::from_settings(&settings); - - // URL containing multiple domains (edge case - should only replace first match) - let url = "https://tpc.googlesyndication.com/path?redirect=https://securepubads.g.doubleclick.net/other"; - let rewritten = manager.rewrite_url(url); - - // Only the first domain should be replaced due to the break statement - assert!(rewritten.contains("creatives.auburndao.com")); - assert!(rewritten.contains("/path?redirect=")); - } - - #[test] - fn test_content_rewriting_with_protocol_variations() { - let settings = create_test_settings(); - let manager = PartnerManager::from_settings(&settings); - - let content = r#" - http://tpc.googlesyndication.com/image.jpg - https://tpc.googlesyndication.com/image2.jpg - //tpc.googlesyndication.com/image3.jpg - src="tpc.googlesyndication.com/image4.jpg" - "#; - - let rewritten = manager.rewrite_content(content); - - assert!(rewritten.contains("http://creatives.auburndao.com/image.jpg")); - assert!(rewritten.contains("https://creatives.auburndao.com/image2.jpg")); - assert!(rewritten.contains("//creatives.auburndao.com/image3.jpg")); - assert!(rewritten.contains("src=\"creatives.auburndao.com/image4.jpg\"")); - } - - #[test] - fn test_case_sensitive_domain_matching() { - let settings = create_test_settings(); - let manager = PartnerManager::from_settings(&settings); - - // Domain matching should be case-sensitive - let url = "https://TPC.GOOGLESYNDICATION.COM/test"; - let rewritten = manager.rewrite_url(url); - assert_eq!(rewritten, url); // Should not be rewritten due to case mismatch - - let url_lower = "https://tpc.googlesyndication.com/test"; - let rewritten_lower = manager.rewrite_url(url_lower); - assert!(rewritten_lower.contains("creatives.auburndao.com")); - } - - #[test] - fn test_partial_domain_matching() { - let settings = create_test_settings(); - let manager = PartnerManager::from_settings(&settings); - - // The current implementation does substring replacement, which can match partial domains - let content = r#" - https://notsecurepubads.g.doubleclick.net/test - https://securepubads.g.doubleclick.net.evil.com/test - https://fake-tpc.googlesyndication.com/test - "#; - - let rewritten = manager.rewrite_content(content); - - // Due to substring replacement, partial matches will occur - // "securepubads.g.doubleclick.net" within "notsecurepubads.g.doubleclick.net" gets replaced - assert!(rewritten.contains("notcreatives.auburndao.com/test")); - // "securepubads.g.doubleclick.net" within the URL gets replaced, leaving ".evil.com" - assert!(rewritten.contains("creatives.auburndao.com.evil.com/test")); - // "tpc.googlesyndication.com" within "fake-tpc.googlesyndication.com" gets replaced - assert!(rewritten.contains("fake-creatives.auburndao.com/test")); - } - - #[test] - fn test_overlapping_domain_configurations() { - let mut settings = Settings::default(); - - // Create configs with overlapping proxy domains - let gam_config = PartnerConfig { - enabled: true, - name: "GAM".to_string(), - domains_to_proxy: vec!["gam.example.com".to_string()], - proxy_domain: "proxy.domain.com".to_string(), - backend_name: "gam_backend".to_string(), - }; - - let equativ_config = PartnerConfig { - enabled: true, - name: "Equativ".to_string(), - domains_to_proxy: vec!["equativ.example.com".to_string()], - proxy_domain: "proxy.domain.com".to_string(), // Same proxy domain - backend_name: "equativ_backend".to_string(), - }; - - settings.partners = Some(Partners { - gam: Some(gam_config), - equativ: Some(equativ_config), - prebid: None, - }); - - let manager = PartnerManager::from_settings(&settings); - - // Both domains should map to the same proxy domain but different backends - assert_eq!( - manager.rewrite_url("https://gam.example.com/path"), - "https://proxy.domain.com/path" - ); - assert_eq!( - manager.rewrite_url("https://equativ.example.com/path"), - "https://proxy.domain.com/path" - ); - assert_eq!( - manager.get_backend_for_domain("gam.example.com"), - Some("gam_backend") - ); - assert_eq!( - manager.get_backend_for_domain("equativ.example.com"), - Some("equativ_backend") - ); - } -} diff --git a/crates/common/src/prebid.rs b/crates/common/src/prebid.rs deleted file mode 100644 index d1a486f..0000000 --- a/crates/common/src/prebid.rs +++ /dev/null @@ -1,467 +0,0 @@ -//! Prebid integration for real-time bidding. -//! -//! This module provides functionality for integrating with Prebid Server -//! to enable header bidding and real-time ad auctions. - -use error_stack::Report; -use fastly::http::{header, Method, StatusCode}; -use fastly::{Error, Request, Response}; -use serde_json::json; - -use crate::backend::ensure_backend_from_url; -use crate::constants::{ - HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_COMPRESS_HINT, - HEADER_X_CONSENT_ADVERTISING, HEADER_X_FORWARDED_FOR, -}; -use crate::error::TrustedServerError; -use crate::settings::Settings; -use crate::synthetic::{generate_synthetic_id, get_or_generate_synthetic_id}; - -/// Represents a request to the Prebid Server with all necessary parameters -pub struct PrebidRequest { - /// Synthetic ID used for user identification across requests - pub synthetic_id: String, - /// Domain for the ad request - pub domain: String, - /// List of banner sizes as (width, height) tuples - pub banner_sizes: Vec<(u32, u32)>, - /// Client's IP address for geo-targeting and fraud prevention - pub client_ip: String, - /// Origin header for CORS and tracking - pub origin: String, -} - -impl PrebidRequest { - /// Creates a new PrebidRequest from an incoming Fastly request. - /// - /// Extracts necessary information from the request including synthetic ID, - /// client IP, and origin for use in Prebid Server requests. - /// - /// # Errors - /// - /// - [`TrustedServerError::SyntheticId`] if synthetic ID generation fails - pub fn new(settings: &Settings, req: &Request) -> Result> { - // Get the Trusted Server ID from header (which we just set in handle_prebid_test) - let synthetic_id = match req - .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) - .and_then(|h| h.to_str().ok()) - .map(|s| s.to_string()) - { - Some(id) => id, - None => generate_synthetic_id(settings, req)?, - }; - - // Get the original client IP from Fastly headers - let client_ip = req - .get_client_ip_addr() - .map(|ip| ip.to_string()) - .unwrap_or_else(|| { - req.get_header(HEADER_X_FORWARDED_FOR) - .and_then(|h| h.to_str().ok()) - .unwrap_or("") - .split(',') // X-Forwarded-For can be a comma-separated list - .next() // Get the first (original) IP - .unwrap_or("") - .to_string() - }); - - // Try to get domain from Referer or Origin headers, fallback to default - let domain = req - .get_header(header::REFERER) - .and_then(|h| h.to_str().ok()) - .and_then(|r| url::Url::parse(r).ok()) - .and_then(|u| u.host_str().map(|h| h.to_string())) - .or_else(|| { - req.get_header(header::ORIGIN) - .and_then(|h| h.to_str().ok()) - .and_then(|o| url::Url::parse(o).ok()) - .and_then(|u| u.host_str().map(|h| h.to_string())) - }) - .unwrap_or_else(|| settings.publisher.domain.clone()); - - log::info!("Detected domain: {}", domain); - - // Create origin with owned String - let origin = req - .get_header(header::ORIGIN) - .and_then(|h| h.to_str().ok()) - .map(|s| s.to_string()) - .unwrap_or_else(|| format!("https://{}", domain)); - - Ok(Self { - synthetic_id, - domain, - banner_sizes: vec![(728, 90)], // TODO: Make this configurable - client_ip, - origin, - }) - } - - /// Sends bid request to Prebid Server - /// - /// Makes an HTTP POST request to PBS with all necessary headers and body - /// Uses the stored synthetic ID for user identification - /// - /// # Returns - /// * `Result` - Prebid Server response or error - pub async fn send_bid_request( - &self, - settings: &Settings, - incoming_req: &Request, - ) -> Result { - let mut req = Request::new(Method::POST, settings.prebid.server_url.to_owned()); - - // Get and store the POTSI ID value from the incoming request - let id: String = incoming_req - .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) - .and_then(|h| h.to_str().ok()) - .map(|s| s.to_string()) - .unwrap_or_else(|| self.synthetic_id.clone()); - - log::info!("Found Trusted Server ID from incoming request: {}", id); - - // Construct the OpenRTB2 bid request - let prebid_body = json!({ - "id": id, - "imp": [{ - "id": "imp1", - "banner": { - "format": self.banner_sizes.iter().map(|(w, h)| { - json!({ "w": w, "h": h }) - }).collect::>() - }, - "bidfloor": 0.01, - "bidfloorcur": "USD", - "ext": { - "prebid": { - "debug": true, - "trace": "verbose", - "bidder": { - "smartadserver": { - "siteId": 686105, - "networkId": 5280, - "pageId": 2040327, - "formatId": 137675, - "target": "testing=prebid", - "domain": &self.domain - } - } - } - } - }], - "site": { "page": format!("https://{}", self.domain) }, - "user": { - "id": "5280", - "ext": { - "eids": [ - { - "source": &self.domain, - "uids": [{ - "id": self.synthetic_id, - "atype": 1, - "ext": { - "type": "fresh" - } - }], - }, - { - "source": &self.domain, - "uids": [{ - "id": &id, - "atype": 1, - "ext": { - "type": "potsi" // TODO: remove reference to potsi - } - }] - } - ] - } - }, - "test": true, - "debug": true, - "tmax": 1000, - "at": 1 - }); - - req.set_header(header::CONTENT_TYPE, "application/json"); - req.set_header(HEADER_X_FORWARDED_FOR, &self.client_ip); - req.set_header(header::ORIGIN, &self.origin); - req.set_header(HEADER_SYNTHETIC_FRESH, &self.synthetic_id); - req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, &id); - - log::info!( - "Sending prebid request with Fresh ID: {} and Trusted Server ID: {}", - self.synthetic_id, - id - ); - - // TrustedServerError doesn't implement std::error::Error - match ensure_backend_from_url(&settings.prebid.server_url) { - Ok(backend_name) => { - req.set_body_json(&prebid_body)?; - - let resp = req.send(backend_name)?; - Ok(resp) - } - - Err(e) => fastly::error::bail!("Could not get prebid backend: {}", e), - } - } -} - -/// Handles the prebid test route with detailed error logging. -/// -/// This endpoint is used to test Prebid Server integration by: -/// 1. Checking consent status -/// 2. Generating synthetic IDs (if consent is given) -/// 3. Creating a PrebidRequest -/// 4. Sending the bid request to Prebid Server -/// 5. Returning the response with appropriate headers -/// -/// # Errors -/// -/// Returns a [`TrustedServerError`] if: -/// - Synthetic ID generation fails -/// - PrebidRequest creation fails -/// - Communication with Prebid Server fails -pub async fn handle_prebid_test( - settings: &Settings, - mut req: Request, -) -> Result> { - log::info!("Starting prebid test request handling"); - - // Check consent status from headers - let advertising_consent = req - .get_header(HEADER_X_CONSENT_ADVERTISING) - .and_then(|h| h.to_str().ok()) - .map(|v| v == "true") - .unwrap_or(false); - - // Calculate fresh ID and synthetic ID only if we have advertising consent - let (fresh_id, synthetic_id) = if advertising_consent { - let fresh = generate_synthetic_id(settings, &req)?; - let synth = get_or_generate_synthetic_id(settings, &req)?; - (fresh, synth) - } else { - // Use non-personalized IDs when no consent - ( - "non-personalized".to_string(), - "non-personalized".to_string(), - ) - }; - - log::info!( - "Existing Trusted Server header: {:?}", - req.get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) - ); - log::info!("Generated Fresh ID: {}", &fresh_id); - log::info!("Using Trusted Server ID: {}", synthetic_id); - log::info!("Advertising consent: {}", advertising_consent); - - // Set both IDs as headers - req.set_header(HEADER_SYNTHETIC_FRESH, &fresh_id); - req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, &synthetic_id); - req.set_header( - HEADER_X_CONSENT_ADVERTISING, - if advertising_consent { "true" } else { "false" }, - ); - - log::info!( - "Using Trusted Server ID: {}, Fresh ID: {}", - synthetic_id, - fresh_id - ); - - let prebid_req = PrebidRequest::new(settings, &req)?; - log::info!( - "Successfully created PrebidRequest with synthetic ID: {}", - prebid_req.synthetic_id - ); - - log::info!("Attempting to send bid request to Prebid Server at prebid_backend"); - - match prebid_req.send_bid_request(settings, &req).await { - Ok(mut prebid_response) => { - log::info!("Received response from Prebid Server"); - log::info!("Response status: {}", prebid_response.get_status()); - - log::info!("Response headers:"); - for (name, value) in prebid_response.get_headers() { - log::info!(" {}: {:?}", name, value); - } - - let body = prebid_response.take_body_str(); - log::info!("Response body: {}", body); - - Ok(Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_header("X-Prebid-Test", "true") - .with_header("X-Synthetic-ID", &prebid_req.synthetic_id) - .with_header( - "X-Consent-Advertising", - if advertising_consent { "true" } else { "false" }, - ) - .with_header(HEADER_X_COMPRESS_HINT, "on") - .with_body(body)) - } - Err(e) => { - log::error!("Error sending bid request: {:?}", e); - log::error!("Backend name used: prebid_backend"); - - // Convert Fastly Error to TrustedServerError - Err(Report::new(TrustedServerError::Prebid { - message: format!("Failed to send bid request: {}", e), - })) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fastly::Request; - - use crate::test_support::tests::create_test_settings; - - #[test] - fn test_prebid_request_new_with_full_headers() { - let settings = create_test_settings(); - let mut req = Request::get("https://example.com/test"); - req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, "existing-synthetic-id"); - req.set_header(header::REFERER, "https://test-domain.com/page"); - req.set_header(header::ORIGIN, "https://test-domain.com"); - req.set_header(HEADER_X_FORWARDED_FOR, "192.168.1.1, 10.0.0.1"); - - let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); - - assert_eq!(prebid_req.synthetic_id, "existing-synthetic-id"); - assert_eq!(prebid_req.domain, "test-domain.com"); - assert_eq!(prebid_req.banner_sizes, vec![(728, 90)]); - assert_eq!(prebid_req.origin, "https://test-domain.com"); - // Note: client_ip extraction from X-Forwarded-For depends on Fastly runtime - } - - #[test] - fn test_prebid_request_new_without_synthetic_id() { - let settings = create_test_settings(); - let mut req = Request::get("https://example.com/test"); - req.set_header("User-Agent", "Mozilla/5.0"); - req.set_header(header::REFERER, "https://test-domain.com/page"); - - let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); - - // Should generate a new synthetic ID - assert!(!prebid_req.synthetic_id.is_empty()); - assert_eq!(prebid_req.domain, "test-domain.com"); - } - - #[test] - fn test_prebid_request_domain_from_origin() { - let settings = create_test_settings(); - let mut req = Request::get("https://example.com/test"); - req.set_header(header::ORIGIN, "https://origin-domain.com"); - // No referer header - - let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); - - assert_eq!(prebid_req.domain, "origin-domain.com"); - assert_eq!(prebid_req.origin, "https://origin-domain.com"); - } - - #[test] - fn test_prebid_request_domain_fallback() { - let settings = create_test_settings(); - let url = format!("https://{}", settings.publisher.domain); - let req = Request::get(url.clone()); - // No referer or origin headers - - let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); - - assert_eq!(prebid_req.domain, settings.publisher.domain); - assert_eq!(prebid_req.origin, url); - } - - #[test] - fn test_prebid_request_invalid_url_in_referer() { - let settings = create_test_settings(); - let url = format!("https://{}/test", settings.publisher.domain); - let mut req = Request::get(url); - req.set_header(header::REFERER, "not-a-valid-url"); - - let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); - - // Should fallback to default domain - assert_eq!(prebid_req.domain, settings.publisher.domain); - } - - #[test] - fn test_prebid_request_x_forwarded_for_parsing() { - let settings = create_test_settings(); - let url = format!("https://{}/test", settings.publisher.domain); - let mut req = Request::get(url); - req.set_header(HEADER_X_FORWARDED_FOR, "192.168.1.1, 10.0.0.1, 172.16.0.1"); - - let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); - - // Should get the first IP from the list (if get_client_ip_addr returns None) - // The actual behavior depends on Fastly runtime - assert!(!prebid_req.client_ip.is_empty()); - } - - #[test] - fn test_prebid_request_struct_fields() { - let prebid_req = PrebidRequest { - synthetic_id: "test-id".to_string(), - domain: "test.com".to_string(), - banner_sizes: vec![(300, 250), (728, 90)], - client_ip: "192.168.1.1".to_string(), - origin: "https://test.com".to_string(), - }; - - assert_eq!(prebid_req.synthetic_id, "test-id"); - assert_eq!(prebid_req.domain, "test.com"); - assert_eq!(prebid_req.banner_sizes.len(), 2); - assert_eq!(prebid_req.banner_sizes[0], (300, 250)); - assert_eq!(prebid_req.banner_sizes[1], (728, 90)); - assert_eq!(prebid_req.client_ip, "192.168.1.1"); - assert_eq!(prebid_req.origin, "https://test.com"); - } - - #[test] - fn test_prebid_request_with_multiple_sizes() { - let mut prebid_req = PrebidRequest { - synthetic_id: "test-id".to_string(), - domain: "test.com".to_string(), - banner_sizes: vec![(300, 250), (728, 90), (160, 600)], - client_ip: "192.168.1.1".to_string(), - origin: "https://test.com".to_string(), - }; - - // Test modifying banner sizes - prebid_req.banner_sizes.push((970, 250)); - assert_eq!(prebid_req.banner_sizes.len(), 4); - assert_eq!(prebid_req.banner_sizes[3], (970, 250)); - } - - #[test] - fn test_prebid_request_edge_cases() { - let settings = create_test_settings(); - let url = format!("https://{}/test", settings.publisher.domain); - - // Test with empty X-Forwarded-For - let mut req = Request::get(url.clone()); - req.set_header(HEADER_X_FORWARDED_FOR, ""); - let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); - assert!(!prebid_req.client_ip.is_empty() || prebid_req.client_ip.is_empty()); - - // Test with malformed origin - let mut req2 = Request::get(url.clone()); - req2.set_header(header::ORIGIN, "://invalid"); - let prebid_req2 = PrebidRequest::new(&settings, &req2).unwrap(); - assert_eq!(prebid_req2.domain, settings.publisher.domain); - } - - // Note: Testing send_bid_request would require mocking the Fastly backend, - // which isn't available in unit tests. This would be covered in integration tests. - // The method constructs a proper OpenRTB request with all required fields. -} diff --git a/crates/common/src/prebid_proxy.rs b/crates/common/src/prebid_proxy.rs index 180dbf7..745d03c 100644 --- a/crates/common/src/prebid_proxy.rs +++ b/crates/common/src/prebid_proxy.rs @@ -12,7 +12,6 @@ use serde_json::{json, Value}; use crate::backend::ensure_backend_from_url; use crate::constants::{HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER}; use crate::error::TrustedServerError; -use crate::gdpr::get_consent_from_request; use crate::settings::Settings; use crate::synthetic::{generate_synthetic_id, get_or_generate_synthetic_id}; @@ -129,162 +128,6 @@ pub async fn handle_prebid_auction( } } -/// Handles cookie sync requests for Prebid Server. -/// -/// This ensures user syncing happens through the first-party domain. -/// -/// # Errors -/// -/// Returns a [`TrustedServerError`] if: -/// - Request parsing fails -/// - Communication with Prebid Server fails -pub async fn handle_prebid_cookie_sync( - settings: &Settings, - mut req: Request, -) -> Result> { - log::info!("Handling Prebid cookie sync request"); - - // 1. Parse sync request - let mut sync_request: Value = serde_json::from_slice(&req.take_body_bytes()).change_context( - TrustedServerError::Prebid { - message: "Failed to parse cookie sync request".to_string(), - }, - )?; - - // 2. Add privacy signals - if let Some(consent) = get_consent_from_request(&req) { - sync_request["gdpr"] = json!(1); - // Use placeholder TCF string if advertising consent is given - if consent.advertising { - sync_request["gdpr_consent"] = json!("CPZnoGVPZnoGVAfEjBENCZCsAP_AAE_AAAAAYgJNNf_X__b3_j-_5_f_t0eY1P9_7_v-0zjhQNA_gAAAAAAAAAAAAAAAAAAAA"); - } - } - - // 3. Forward to PBS - let mut pbs_req = Request::new( - Method::POST, - format!("{}/cookie_sync", settings.prebid.server_url), - ); - - // Pass through cookies (especially PBS uids cookie) - if let Some(cookie_header) = req.get_header(header::COOKIE) { - pbs_req.set_header(header::COOKIE, cookie_header); - } - - pbs_req - .set_body_json(&sync_request) - .change_context(TrustedServerError::Prebid { - message: "Failed to set sync request body".to_string(), - })?; - - // 4. Get response and transform sync URLs - - let backend_name = ensure_backend_from_url(&settings.prebid.server_url)?; - - let mut pbs_response = - pbs_req - .send(backend_name) - .change_context(TrustedServerError::Prebid { - message: "Failed to send cookie sync request".to_string(), - })?; - - if pbs_response.get_status().is_success() { - let response_body = pbs_response.take_body_bytes(); - - match serde_json::from_slice::(&response_body) { - Ok(mut sync_data) => { - // Transform sync URLs to first-party - let request_host = get_request_host(&req); - let request_scheme = get_request_scheme(&req); - - if let Some(syncs) = sync_data["syncs"].as_array_mut() { - for sync in syncs { - if let Some(url) = sync["url"].as_str() { - sync["url"] = json!(make_first_party_proxy_url( - url, - &request_host, - &request_scheme, - "sync" - )); - } - } - } - - let transformed_body = - serde_json::to_vec(&sync_data).change_context(TrustedServerError::Prebid { - message: "Failed to serialize sync response".to_string(), - })?; - - Ok(Response::from_status(StatusCode::OK) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body(transformed_body)) - } - Err(_) => { - // Pass through as-is if not JSON - Ok(Response::from_status(pbs_response.get_status()) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body(response_body)) - } - } - } else { - Ok(pbs_response) - } -} - -/// Handles proxying of ad creatives and other resources through the first-party domain. -/// -/// # Errors -/// -/// Returns a [`TrustedServerError`] if: -/// - URL decoding fails -/// - Backend request fails -pub async fn handle_ad_proxy( - _settings: &Settings, - req: Request, - path: &str, -) -> Result> { - // Extract proxy type and encoded URL - let parts: Vec<&str> = path.trim_start_matches("/ad-proxy/").split('/').collect(); - if parts.len() < 2 { - return Err(Report::new(TrustedServerError::BadRequest { - message: "Invalid proxy request".to_string(), - })); - } - - let proxy_type = parts[0]; - let encoded_url = parts[1]; - - // Decode the actual URL - let actual_url = BASE64 - .decode(encoded_url) - .change_context(TrustedServerError::Proxy { - message: "Failed to decode proxy URL".to_string(), - }) - .and_then(|bytes| { - String::from_utf8(bytes).change_context(TrustedServerError::Proxy { - message: "Invalid UTF-8 in decoded URL".to_string(), - }) - })?; - - log::info!("Proxying {} request to: {}", proxy_type, actual_url); - - // Determine backend based on URL - let backend = determine_backend_for_url(&actual_url); - - // Create request to actual URL - let mut proxy_req = Request::new(req.get_method().clone(), actual_url); - - // Copy relevant headers - copy_proxy_headers(&req, &mut proxy_req); - - // Send request and return response - proxy_req - .send(backend) - .change_context(TrustedServerError::Proxy { - message: format!("Failed to proxy request to {}", backend), - }) -} - /// Enhances the OpenRTB request with synthetic IDs and privacy information. fn enhance_openrtb_request( request: &mut Value, @@ -305,22 +148,7 @@ fn enhance_openrtb_request( } request["user"]["ext"]["synthetic_fresh"] = json!(fresh_id); - // Add privacy signals - if let Some(consent) = get_consent_from_request(req) { - if !request["regs"].is_object() { - request["regs"] = json!({}); - } - if !request["regs"]["ext"].is_object() { - request["regs"]["ext"] = json!({}); - } - request["regs"]["ext"]["gdpr"] = json!(1); - - // For now, use a placeholder TCF string if advertising consent is given - // In production, this should come from a proper CMP - if consent.advertising { - request["user"]["ext"]["consent"] = json!("CPZnoGVPZnoGVAfEjBENCZCsAP_AAE_AAAAAYgJNNf_X__b3_j-_5_f_t0eY1P9_7_v-0zjhQNA_gAAAAAAAAAAAAAAAAAAAA"); - } - } + // TODO: Add privacy signals // Add US Privacy if present if req.get_header("Sec-GPC").is_some() { @@ -460,23 +288,6 @@ fn copy_request_headers(from: &Request, to: &mut Request) { } } -/// Copies headers appropriate for proxying to ad servers. -fn copy_proxy_headers(from: &Request, to: &mut Request) { - let headers_to_copy = [ - header::USER_AGENT, - header::ACCEPT, - header::ACCEPT_LANGUAGE, - header::ACCEPT_ENCODING, - header::HeaderName::from_static("x-forwarded-for"), - ]; - - for header_name in &headers_to_copy { - if let Some(value) = from.get_header(header_name) { - to.set_header(header_name, value); - } - } -} - /// Gets the request host from the incoming request. fn get_request_host(req: &Request) -> String { req.get_header(header::HOST) @@ -501,21 +312,8 @@ fn get_request_scheme(req: &Request) -> String { "https".to_string() // Default to HTTPS for security } -/// Determines the appropriate backend for a given URL. -fn determine_backend_for_url(url: &str) -> &'static str { - if url.contains("doubleclick.net") { - "gam_backend" - } else { - "ad_cdn_backend" // Default backend for all other ad servers - } -} - #[cfg(test)] mod tests { - - // Note: test_make_first_party_proxy_url removed as it tested a private function. - // This functionality is tested through the public handle_ad_proxy function. - // Note: test_rewrite_ad_markup removed as it tested a private function. // This functionality is tested through the public handle_prebid_auction function. diff --git a/crates/common/src/privacy.rs b/crates/common/src/privacy.rs deleted file mode 100644 index 4a25fcc..0000000 --- a/crates/common/src/privacy.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Privacy policy handling. -//! -//! This module provides the privacy policy template and handler function. - -use error_stack::Report; -use fastly::http::{header, StatusCode}; -use fastly::{Request, Response}; - -use crate::constants::HEADER_X_COMPRESS_HINT; -use crate::error::TrustedServerError; -use crate::settings::Settings; - -/// Handles privacy policy page requests. -/// -/// Returns the privacy policy HTML page. -/// -/// # Errors -/// -/// This function currently doesn't return errors, but returns a `Result` for consistency -/// and future extensibility. -pub fn handle_privacy_policy( - _settings: &Settings, - _req: Request, -) -> Result> { - Ok(Response::from_status(StatusCode::OK) - .with_body(PRIVACY_TEMPLATE) - .with_header(header::CONTENT_TYPE, "text/html") - .with_header(HEADER_X_COMPRESS_HINT, "on")) -} - -pub const PRIVACY_TEMPLATE: &str = r#" - - - - - Privacy Policy - Travel Southeast Asia - - - - ← Back to Home -
-

Privacy Policy

- -
-

1. Introduction

-

This Privacy Policy explains how we collect, use, and protect your personal information when you use our website. We are committed to ensuring your privacy and protecting your data in compliance with GDPR and other applicable data protection laws.

-
- -
-

2. Information We Collect

-

We collect the following types of information:

-
    -
  • Synthetic IDs for tracking website usage
  • -
  • Cookie preferences and consent choices
  • -
  • Browser information and technical data
  • -
  • IP addresses (anonymized for analytics)
  • -
  • Usage data and interaction with advertisements
  • -
-
- -
-

3. How We Use Your Information

-

We use your information for:

-
    -
  • Providing and improving our services
  • -
  • Personalizing content and advertisements
  • -
  • Analyzing website traffic and user behavior
  • -
  • Ensuring website security and preventing fraud
  • -
-
- -
-

4. Cookie Policy

-

We use different types of cookies:

-
    -
  • Functional Cookies: Essential for the website to work properly
  • -
  • Analytics Cookies: Help us understand how visitors use our site
  • -
  • Advertising Cookies: Used to deliver relevant advertisements
  • -
-

You can manage your cookie preferences through our consent banner or preferences center.

-
- -
-

5. Your Rights

-

Under GDPR, you have the following rights:

-
    -
  • Right to access your personal data
  • -
  • Right to rectification of incorrect data
  • -
  • Right to erasure ("right to be forgotten")
  • -
  • Right to restrict processing
  • -
  • Right to data portability
  • -
  • Right to object to processing
  • -
-

To exercise these rights, please contact us using the information below.

-
- -
-

6. Data Retention

-

We retain your personal data only for as long as necessary to fulfill the purposes for which it was collected. Synthetic IDs and related data are stored for a maximum of 13 months.

-
- -
-

7. Contact Information

-

For any privacy-related questions or requests, please contact us at:

-

Email: privacy@auburndao.com
- Address: 123 Privacy Street, Data City, 12345

-
- -

Last Updated: March 24, 2024

-
- -"#; diff --git a/crates/common/src/publisher.rs b/crates/common/src/publisher.rs index 6fda0a3..2820d87 100644 --- a/crates/common/src/publisher.rs +++ b/crates/common/src/publisher.rs @@ -5,20 +5,13 @@ use fastly::{Body, Request, Response}; use crate::backend::ensure_backend_from_url; use crate::http_util::serve_static_with_etag; -use crate::constants::{ - HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_COMPRESS_HINT, - HEADER_X_GEO_CITY, HEADER_X_GEO_CONTINENT, HEADER_X_GEO_COORDINATES, HEADER_X_GEO_COUNTRY, - HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE, -}; +use crate::constants::{HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_COMPRESS_HINT}; use crate::cookies::create_synthetic_cookie; use crate::error::TrustedServerError; -use crate::gdpr::{get_consent_from_request, GdprConsent}; -use crate::geo::get_dma_code; use crate::settings::Settings; use crate::streaming_processor::{Compression, PipelineConfig, StreamProcessor, StreamingPipeline}; use crate::streaming_replacer::create_url_replacer; -use crate::synthetic::{generate_synthetic_id, get_or_generate_synthetic_id}; -use crate::templates::{EDGEPUBS_TEMPLATE, HTML_TEMPLATE}; +use crate::synthetic::get_or_generate_synthetic_id; /// Detects the request scheme (HTTP or HTTPS) using Fastly SDK methods and headers. /// @@ -79,118 +72,6 @@ fn detect_request_scheme(req: &Request) -> String { "http".to_string() } -// Prebid URL interception logic removed; HTML rewriting handles Prebid script references - -/// Handles the main page request. -/// -/// Serves the main page with synthetic ID generation and ad integration. -/// -/// # Errors -/// -/// Returns a [`TrustedServerError`] if: -/// - Synthetic ID generation fails -/// - Response creation fails -pub fn handle_main_page( - settings: &Settings, - mut req: Request, -) -> Result> { - log::info!( - "Using ad_partner_url: {}, counter_store: {}", - settings.ad_server.ad_partner_url, - settings.synthetic.counter_store, - ); - - // Add DMA code check to main page as well - let dma_code = get_dma_code(&mut req); - log::info!("Main page - DMA Code: {:?}", dma_code); - - // Check GDPR consent before proceeding - let consent = match get_consent_from_request(&req) { - Some(c) => c, - None => { - log::debug!("No GDPR consent found, using default"); - GdprConsent::default() - } - }; - if !consent.functional { - // Return a version of the page without tracking - return Ok(Response::from_status(StatusCode::OK) - .with_body( - HTML_TEMPLATE.replace("fetch('/prebid-test')", "console.log('Tracking disabled')"), - ) - .with_header(header::CONTENT_TYPE, "text/html") - .with_header(header::CACHE_CONTROL, "no-store, private")); - } - - // Calculate fresh ID first using the synthetic module - let fresh_id = generate_synthetic_id(settings, &req)?; - - // Check for existing Trusted Server ID in this specific order: - // 1. X-Synthetic-Trusted-Server header - // 2. Cookie - // 3. Fall back to fresh ID - let synthetic_id = get_or_generate_synthetic_id(settings, &req)?; - - log::info!( - "Existing Trusted Server header: {:?}", - req.get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) - ); - log::info!("Generated Fresh ID: {}", &fresh_id); - log::info!("Using Trusted Server ID: {}", synthetic_id); - - // Create response with the main page HTML - let mut response = Response::from_status(StatusCode::OK) - .with_body(HTML_TEMPLATE) - .with_header(header::CONTENT_TYPE, "text/html") - .with_header(HEADER_SYNTHETIC_FRESH, fresh_id.as_str()) // Fresh ID always changes - .with_header(HEADER_SYNTHETIC_TRUSTED_SERVER, &synthetic_id) // Trusted Server ID remains stable - .with_header( - header::ACCESS_CONTROL_EXPOSE_HEADERS, - "X-Geo-City, X-Geo-Country, X-Geo-Continent, X-Geo-Coordinates, X-Geo-Metro-Code, X-Geo-Info-Available" - ) - .with_header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") - .with_header("x-compress-hint", "on"); - - // Copy geo headers from request to response - for header_name in &[ - HEADER_X_GEO_CITY, - HEADER_X_GEO_COUNTRY, - HEADER_X_GEO_CONTINENT, - HEADER_X_GEO_COORDINATES, - HEADER_X_GEO_METRO_CODE, - HEADER_X_GEO_INFO_AVAILABLE, - ] { - if let Some(value) = req.get_header(header_name) { - response.set_header(header_name, value); - } - } - - // Only set cookies if we have consent - if consent.functional { - response.set_header( - header::SET_COOKIE, - create_synthetic_cookie(settings, &synthetic_id), - ); - } - - // Debug: Print all request headers - log::info!("All Request Headers:"); - for (name, value) in req.get_headers() { - log::info!("{}: {:?}", name, value); - } - - // Debug: Print the response headers - log::info!("Response Headers:"); - for (name, value) in response.get_headers() { - log::info!("{}: {:?}", name, value); - } - - // Prevent caching - response.set_header(header::CACHE_CONTROL, "no-store, private"); - - Ok(response) -} - /// Unified tsjs static serving: `/static/tsjs=` /// Accepts: `tsjs-core(.min).js`, `tsjs-ext(.min).js`, `tsjs-creative(.min).js` pub fn handle_tsjs_dynamic( @@ -461,63 +342,6 @@ pub fn handle_publisher_request( Ok(response) } -/// Handles the EdgePubs page request. -/// -/// Serves the EdgePubs landing page with integrated ad slots. -/// -/// # Errors -/// -/// Returns a [`TrustedServerError`] if response creation fails. -pub fn handle_edgepubs_page( - settings: &Settings, - mut req: Request, -) -> Result> { - log::info!("Serving EdgePubs landing page"); - - // log_fastly::init_simple("mylogs", Info); - - // Add DMA code check - let dma_code = get_dma_code(&mut req); - log::info!("EdgePubs page - DMA Code: {:?}", dma_code); - - // Check GDPR consent - let _consent = match get_consent_from_request(&req) { - Some(c) => c, - None => { - log::debug!("No GDPR consent found for EdgePubs page, using default"); - GdprConsent::default() - } - }; - - // Generate synthetic ID for EdgePubs page - let fresh_id = generate_synthetic_id(settings, &req)?; - - // Get or generate Trusted Server ID - let trusted_server_id = get_or_generate_synthetic_id(settings, &req)?; - - // Create response with EdgePubs template - let mut response = Response::from_status(StatusCode::OK) - .with_body(EDGEPUBS_TEMPLATE) - .with_header(header::CONTENT_TYPE, "text/html") - .with_header(header::CACHE_CONTROL, "no-store, private") - .with_header(HEADER_X_COMPRESS_HINT, "on"); - - // Add synthetic ID headers - response.set_header(HEADER_SYNTHETIC_FRESH, &fresh_id); - response.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, &trusted_server_id); - - // Add DMA code header if available - if let Some(dma) = dma_code { - response.set_header(HEADER_X_GEO_METRO_CODE, dma); - } - - // Set synthetic ID cookie - let cookie = create_synthetic_cookie(settings, &trusted_server_id); - response.set_header(header::SET_COOKIE, cookie); - - Ok(response) -} - #[cfg(test)] mod tests { use super::*; @@ -624,17 +448,6 @@ mod tests { } } - #[test] - fn test_handle_main_page_gdpr_consent() { - let settings = create_test_settings(); - let req = Request::new(Method::GET, "https://example.com/"); - - // Without GDPR consent, tracking should be disabled - let response = handle_main_page(&settings, req).unwrap(); - assert_eq!(response.get_status(), StatusCode::OK); - // Note: Would need to verify response body contains disabled tracking - } - #[test] fn test_publisher_origin_host_extraction() { let settings = create_test_settings(); diff --git a/crates/common/src/settings.rs b/crates/common/src/settings.rs index 3b59a1f..e6fc6ab 100644 --- a/crates/common/src/settings.rs +++ b/crates/common/src/settings.rs @@ -5,19 +5,14 @@ use error_stack::{Report, ResultExt}; use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize}; use serde_json::Value as JsonValue; use url::Url; +use validator::{Validate, ValidationError}; use crate::error::TrustedServerError; pub const ENVIRONMENT_VARIABLE_PREFIX: &str = "TRUSTED_SERVER"; pub const ENVIRONMENT_VARIABLE_SEPARATOR: &str = "__"; -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct AdServer { - pub ad_partner_url: String, - pub sync_url: String, -} - -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize, Validate)] pub struct Publisher { pub domain: String, pub cookie_domain: String, @@ -56,11 +51,9 @@ impl Publisher { } } -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize, Validate)] pub struct Prebid { pub server_url: String, - #[serde(default = "default_account_id")] - pub account_id: String, #[serde(default = "default_timeout_ms")] pub timeout_ms: u32, #[serde(default = "default_bidders", deserialize_with = "vec_from_seq_or_map")] @@ -71,81 +64,46 @@ pub struct Prebid { pub debug: bool, } -fn default_account_id() -> String { - "1001".to_string() -} - fn default_timeout_ms() -> u32 { 1000 } fn default_bidders() -> Vec { - vec![ - "kargo".to_string(), - "rubicon".to_string(), - "appnexus".to_string(), - "openx".to_string(), - ] + vec!["mocktioneer".to_string()] } fn default_auto_configure() -> bool { true } -#[derive(Debug, Default, Deserialize, Serialize)] -#[allow(unused)] -pub struct GamAdUnit { - pub name: String, - pub size: String, -} - -#[derive(Debug, Default, Deserialize, Serialize)] #[allow(unused)] -pub struct Gam { - pub publisher_id: String, - pub server_url: String, - pub ad_units: Vec, -} - -#[allow(unused)] -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize, Validate)] pub struct Synthetic { pub counter_store: String, pub opid_store: String, + #[validate(length(min = 1), custom(function = Synthetic::validate_secret_key))] pub secret_key: String, + #[validate(length(min = 1))] pub template: String, } -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct PartnerConfig { - pub enabled: bool, - pub name: String, - pub domains_to_proxy: Vec, - pub proxy_domain: String, - pub backend_name: String, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct Partners { - pub gam: Option, - pub equativ: Option, - pub prebid: Option, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct Experimental { - pub enable_edge_pub: bool, +impl Synthetic { + pub fn validate_secret_key(secret_key: &str) -> Result<(), ValidationError> { + match secret_key { + "secret_key" => Err(ValidationError::new("Secret key is not valid")), + _ => Ok(()), + } + } } -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize, Validate)] pub struct Settings { - pub ad_server: AdServer, + #[validate(nested)] pub publisher: Publisher, + #[validate(nested)] pub prebid: Prebid, - pub gam: Gam, + #[validate(nested)] pub synthetic: Synthetic, - pub partners: Option, - pub experimental: Option, } #[allow(unused)] @@ -278,9 +236,6 @@ mod tests { assert!(settings.is_ok(), "Settings should load from embedded TOML"); let settings = settings.unwrap(); - // Verify basic structure is loaded - assert!(!settings.ad_server.ad_partner_url.is_empty()); - assert!(!settings.ad_server.sync_url.is_empty()); assert!(!settings.publisher.domain.is_empty()); assert!(!settings.publisher.cookie_domain.is_empty()); @@ -292,10 +247,6 @@ mod tests { assert!(!settings.synthetic.opid_store.is_empty()); assert!(!settings.synthetic.secret_key.is_empty()); assert!(!settings.synthetic.template.is_empty()); - - assert!(!settings.gam.publisher_id.is_empty()); - assert!(!settings.gam.server_url.is_empty()); - assert!(!settings.gam.ad_units.is_empty()); } #[test] @@ -306,14 +257,6 @@ mod tests { assert!(settings.is_ok()); let settings = settings.expect("should parse valid TOML"); - assert_eq!( - settings.ad_server.ad_partner_url, - "https://test-adpartner.com" - ); - assert_eq!( - settings.ad_server.sync_url, - "https://test-adpartner.com/synthetic_id={{synthetic_id}}" - ); assert_eq!( settings.prebid.server_url, "https://test-prebid.com/openrtb2/auction" @@ -329,19 +272,12 @@ mod tests { assert_eq!(settings.synthetic.secret_key, "test-secret-key"); assert!(settings.synthetic.template.contains("{{client_ip}}")); - assert_eq!(settings.gam.publisher_id, "21796327522"); - assert_eq!( - settings.gam.server_url, - "https://securepubads.g.doubleclick.net/gampad/ads" - ); - assert_eq!(settings.gam.ad_units.len(), 2); - assert_eq!(settings.gam.ad_units[0].name, "test_unit_1"); - assert_eq!(settings.gam.ad_units[0].size, "320x50"); + settings.validate().expect("Failed to validate settings"); } #[test] fn test_settings_missing_required_fields() { - let re = Regex::new(r"ad_partner_url = .*").unwrap(); + let re = Regex::new(r"origin_url = .*").unwrap(); let toml_str = crate_test_settings_str(); let toml_str = re.replace(&toml_str, ""); @@ -372,7 +308,7 @@ mod tests { #[test] fn test_settings_partial_config() { - let re = Regex::new(r"\[ad_server\]").unwrap(); + let re = Regex::new(r"\[publisher\]").unwrap(); let toml_str = crate_test_settings_str(); let toml_str = re.replace(&toml_str, ""); @@ -480,19 +416,19 @@ mod tests { temp_env::with_var( format!( - "{}{}AD_SERVER{}AD_PARTNER_URL", + "{}{}PUBLISHER{}ORIGIN_URL", ENVIRONMENT_VARIABLE_PREFIX, ENVIRONMENT_VARIABLE_SEPARATOR, ENVIRONMENT_VARIABLE_SEPARATOR ), - Some("https://change-ad.com/serve"), + Some("https://change-publisher.com"), || { let settings = Settings::from_toml(&toml_str); assert!(settings.is_ok(), "Settings should load from embedded TOML"); assert_eq!( - settings.unwrap().ad_server.ad_partner_url, - "https://change-ad.com/serve" + settings.unwrap().publisher.origin_url, + "https://change-publisher.com" ); }, ); @@ -504,19 +440,19 @@ mod tests { temp_env::with_var( format!( - "{}{}AD_SERVER{}AD_PARTNER_URL", + "{}{}PUBLISHER{}ORIGIN_URL", ENVIRONMENT_VARIABLE_PREFIX, ENVIRONMENT_VARIABLE_SEPARATOR, ENVIRONMENT_VARIABLE_SEPARATOR ), - Some("https://change-ad.com/serve"), + Some("https://change-publisher.com"), || { let settings = Settings::from_toml(&toml_str); assert!(settings.is_ok(), "Settings should load from embedded TOML"); assert_eq!( - settings.unwrap().ad_server.ad_partner_url, - "https://change-ad.com/serve" + settings.unwrap().publisher.origin_url, + "https://change-publisher.com" ); }, ); diff --git a/crates/common/src/settings_data.rs b/crates/common/src/settings_data.rs index e67bf3c..b16c5c8 100644 --- a/crates/common/src/settings_data.rs +++ b/crates/common/src/settings_data.rs @@ -1,5 +1,6 @@ use core::str; use error_stack::{Report, ResultExt}; +use validator::Validate; use crate::error::TrustedServerError; use crate::settings::Settings; @@ -24,10 +25,12 @@ pub fn get_settings() -> Result> { let settings = Settings::from_toml(toml_str)?; - // Validate that the secret key is not the default - if settings.synthetic.secret_key == "secret-key" { - return Err(Report::new(TrustedServerError::InsecureSecretKey)); - } + // Validate the settings + settings + .validate() + .change_context(TrustedServerError::Configuration { + message: "Failed to validate configuration".to_string(), + })?; Ok(settings) } @@ -44,8 +47,6 @@ mod tests { let settings = settings.unwrap(); // Verify basic structure is loaded - assert!(!settings.ad_server.ad_partner_url.is_empty()); - assert!(!settings.ad_server.sync_url.is_empty()); assert!(!settings.publisher.domain.is_empty()); assert!(!settings.publisher.cookie_domain.is_empty()); assert!(!settings.publisher.origin_url.is_empty()); diff --git a/crates/common/src/templates.rs b/crates/common/src/templates.rs deleted file mode 100644 index f416ffc..0000000 --- a/crates/common/src/templates.rs +++ /dev/null @@ -1,1398 +0,0 @@ -use std::collections::HashMap; - -pub const HTML_TEMPLATE: &str = r#" - - - - - Travel Southeast Asia - - - - - -
-
-

Cookie Consent

-

We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies.

-
- - - -
-

For more information, please read our Privacy Policy

-
- - -
-

Cookie Preferences

-
- - -

Essential for the website to function properly. Cannot be disabled.

-
-
- - -

Help us understand how visitors interact with our website.

-
-
- - -

Used to provide you with personalized advertising.

-
-
- -
-
- -
-

Explore the Wonders of Southeast Asia

-
- -
-
- Thailand -

Thailand

-

Experience the vibrant culture and stunning beaches of Thailand.

-
-
- Vietnam -

Vietnam

-

Discover the rich history and breathtaking landscapes of Vietnam.

-
-
- Indonesia -

Indonesia

-

Explore the diverse islands and unique traditions of Indonesia.

-
-
- Malaysia -

Malaysia

-

Enjoy the blend of modernity and nature in Malaysia.

-
-
- - - - - -
- -
- - -"#; - -pub const EDGEPUBS_TEMPLATE: &str = r##" - - - - - EdgePubs - The Edge Is Yours - - - - - -
-

Cookie Consent

-

We use cookies to enhance your browsing experience and serve personalized ads. By clicking "Accept All", you consent to our use of cookies.

-
- - -
-
- - - - -
-
-

The Edge Is Yours

-

Run your site, ads, and data stack server-side — under your domain, on your terms.

- Get Started → -
-
- - -
-
- -
-
- - -
-
-

Why EdgePubs?

- -
-
-
-

Publisher-Controlled Execution

-

Replace slow browser scripts with fast, server-side orchestration. Run your entire site and ad stack at the edge.

-
-
-

1st-Party Data & Identity

-

Protect and activate your first-party data. Build synthetic IDs and pass privacy-compliant signals to your partners.

-
-
-

Server-Side Tagging

-

No more fragile on-page tags. Execute all third-party tags server-side, giving you speed, control, and compliance.

-
-
-

Ad Stack Orchestration

-

Integrate Prebid Server, GAM, and SSPs directly. Manage auctions and measurement server-side for faster performance.

-
-
-

Faster Sites, Better UX

-

Cut page load times in half. Delight users with blazing fast experiences and fewer third-party browser calls.

-
-
- - - -
-
-
- - -
-
-

How It Works

-
-
    -
  • Trusted Server acts as a secure reverse proxy in front of your CMS (WordPress, Drupal, etc.)
  • -
  • Prebid auctions, ad-serving, and consent tools run server-side, not in the browser.
  • -
  • Contextual signals and creative assets are stitched directly into the page at the edge.
  • -
  • Result: More revenue. More control. Better user experience.
  • -
- -
- Publisher → Trusted Server → Ad Tech Partners → User -
-
-
-
- - -
-
-
-
-
-
-
-
- - -
-
-
- - -
- -
-
    -
  • Full control of your execution environment
  • -
  • Server-side identity, consent, and measurement
  • -
  • No more slow, fragile browser tags
  • -
-
- -
-
    -
  • Cleaner supply paths (no intermediaries siphoning value)
  • -
  • Higher-quality inventory with verified user signals
  • -
  • Cookieless targeting ready out-of-the-box
  • -
-
-
-
- - - - - - -"##; - -pub const GAM_TEST_TEMPLATE: &str = r#" - - - - - - GAM Test - Trusted Server - - - -
-

GAM Test - Headless GPT PoC

- -
-

📋 Instructions for Capture & Replay Phase

-

Phase 1 Goal: Capture a complete, successful ad request URL from test-publisher.com and replay it from our server.

-
    -
  1. Open browser dev tools on test-publisher.com
  2. -
  3. Go to Network tab and filter by "g.doubleclick.net"
  4. -
  5. Refresh the page and look for successful ad requests
  6. -
  7. Copy the complete URL with all parameters
  8. -
  9. Use the "Test Golden URL" button below to test it
  10. -
-
- -
-

Phase 1: Capture & Replay (Golden URL)

-

Test the exact captured URL from test-publisher.com to prove network connectivity.

- -
-

Golden URL Test

-

Paste the captured GAM URL from test-publisher.com below and test it:

-
- -
- - - -
-
- -
-

Phase 2: Dynamic Request Building

-

Test dynamic parameter generation with hardcoded prmtvctx value.

- -
-

Dynamic GAM Request

-

Test server-side GAM request with dynamic correlator and synthetic ID.

- - -
-
- -
-

Phase 3: Ad Rendering in iFrame

-

Render the GAM response HTML content in a sandboxed iframe for visual testing.

- -
-

Ad Render Test

-

Test rendering the GAM response as an actual ad in an iframe:

- - - -
-
- -
-

Debug Information

-
-

Request Headers

-
- -

Synthetic ID Status

-
- Checking synthetic ID... -
-
-
-
- - - - -"#; -// GAM Configuration Template -#[allow(dead_code)] -struct GamConfigTemplate { - publisher_id: String, - ad_units: Vec, - page_context: PageContext, - data_providers: Vec, -} -#[allow(dead_code)] -struct AdUnitConfig { - name: String, - sizes: Vec, - position: String, - targeting: HashMap, -} -#[allow(dead_code)] -struct PageContext { - page_type: String, - section: String, - keywords: Vec, -} -#[allow(dead_code)] -enum DataProvider { - Permutive(PermutiveConfig), - Lotame(LotameConfig), - Neustar(NeustarConfig), - Custom(CustomProviderConfig), -} -#[allow(dead_code)] -struct PermutiveConfig {} -#[allow(dead_code)] -struct LotameConfig {} -#[allow(dead_code)] -struct NeustarConfig {} -#[allow(dead_code)] -struct CustomProviderConfig {} -#[allow(dead_code)] -trait DataProviderTrait { - fn get_user_segments(&self, user_id: &str) -> Vec; -} - -#[allow(dead_code)] -struct RequestContext { - user_id: String, - page_url: String, - consent_status: bool, -} - -#[allow(dead_code)] -struct DynamicGamBuilder { - base_config: GamConfigTemplate, - context: RequestContext, - data_providers: Vec>, -} - -// Instead of hardcoded strings, use templates: -// "cust_params": "{{#each data_providers}}{{name}}={{segments}}&{{/each}}puid={{user_id}}" - -// This could generate: -// "permutive=129627,137412...&lotame=segment1,segment2&puid=abc123" - -// let context = data_provider_manager.build_context(&user_id, &request_context); -// let gam_req_with_context = gam_req.with_dynamic_context(context); diff --git a/crates/common/src/test_support.rs b/crates/common/src/test_support.rs index 97db1bc..c2c31f7 100644 --- a/crates/common/src/test_support.rs +++ b/crates/common/src/test_support.rs @@ -4,53 +4,21 @@ pub mod tests { pub fn crate_test_settings_str() -> String { r#" - [ad_server] - ad_partner_url = "https://test-adpartner.com" - sync_url = "https://test-adpartner.com/synthetic_id={{synthetic_id}}" - [publisher] domain = "test-publisher.com" cookie_domain = ".test-publisher.com" origin_backend = "publisher_origin" - origin_url= "https://origin.test-publisher.com" + origin_url = "https://origin.test-publisher.com" proxy_secret = "unit-test-proxy-secret" [prebid] - server_url = "https://test-prebid.com/openrtb2/auction" - - [gam] - publisher_id = "21796327522" - server_url = "https://securepubads.g.doubleclick.net/gampad/ads" - ad_units = [ - { name = "test_unit_1", size = "320x50" }, - { name = "test_unit_2", size = "728x90" }, - ] + server_url = "https://test-prebid.com/openrtb2/auction" [synthetic] counter_store = "test-counter-store" opid_store = "test-opid-store" secret_key = "test-secret-key" template = "{{client_ip}}:{{user_agent}}:{{first_party_id}}:{{auth_user_id}}:{{publisher_domain}}:{{accept_language}}" - - [partners] - [partners.gam] - enabled = true - name = "Google Ad Manager" - domains_to_proxy = [ - "securepubads.g.doubleclick.net", - "tpc.googlesyndication.com", - ] - proxy_domain = "creatives.auburndao.com" - backend_name = "gam_proxy_backend" - - [partners.equativ] - enabled = true - name = "Equativ (Smart AdServer)" - domains_to_proxy = [ - "creatives.sascdn.com" - ] - proxy_domain = "creatives.auburndao.com" - backend_name = "equativ_proxy_backend" "#.to_string() } diff --git a/crates/common/src/why.rs b/crates/common/src/why.rs deleted file mode 100644 index 0015922..0000000 --- a/crates/common/src/why.rs +++ /dev/null @@ -1,252 +0,0 @@ -//! Why Trusted Server page handling. -//! -//! This module provides the "Why Trusted Server" explanation page. - -use error_stack::Report; -use fastly::http::{header, StatusCode}; -use fastly::{Request, Response}; - -use crate::constants::HEADER_X_COMPRESS_HINT; -use crate::error::TrustedServerError; -use crate::settings::Settings; - -/// Handles "Why Trusted Server" page requests. -/// -/// Returns the Why Trusted Server HTML page that explains the purpose and benefits -/// of the trusted server approach. -/// -/// # Errors -/// -/// This function currently doesn't return errors, but returns a `Result` for consistency -/// and future extensibility. -pub fn handle_why_trusted_server( - _settings: &Settings, - _req: Request, -) -> Result> { - Ok(Response::from_status(StatusCode::OK) - .with_body(WHY_TEMPLATE) - .with_header(header::CONTENT_TYPE, "text/html") - .with_header(HEADER_X_COMPRESS_HINT, "on")) -} - -pub const WHY_TEMPLATE: &str = r#" - - - - - Why Trusted Server | Auburn DAO - - - - -
- - -
-

Why Trusted Server

- -
-
-

Premium publishers have lost monetization capabilities due to big-tech browser decisions and reliance on 3rd party javascript. We feel that the ability to use 3rd party code and tags will continue its trend to zero and want to give publishers a privacy-first tool to fight back.

-
-
- -
-

Our Solution

-
-

We propose leveraging first-party privileges and edge-cloud (server-side) technology to help publishers take back control of advertising monetization and user-data security. We allow publishers to enable what is traditionally done via 3rd party code execution in a first party context. We have moved the ad stack out of the browser into a fast, secure edge-cloud environment.

-
-
- -
-

Key Features

-
-
    -
  • Manage Ad Request and Ad Response
  • -
  • Server Side Ad Stitching
  • -
  • Prebid server integration
  • -
  • Edge Cloud initiation and data signals collection
  • -
  • Plugin support for 3P providers for identity and audience, fraud prevention, brand safety
  • -
  • Plug and play into existing programmatic systems (minimal changes)
  • -
-
-
-
-
- -"#; diff --git a/crates/fastly/src/main.rs b/crates/fastly/src/main.rs index 3d4cb02..d0d6c3e 100644 --- a/crates/fastly/src/main.rs +++ b/crates/fastly/src/main.rs @@ -3,26 +3,13 @@ use fastly::{Error, Request, Response}; use log_fastly::Logger; use trusted_server_common::ad::{handle_server_ad, handle_server_ad_get}; -use trusted_server_common::advertiser::handle_ad_request; -use trusted_server_common::gam::{ - handle_gam_asset, handle_gam_custom_url, handle_gam_golden_url, handle_gam_render, - handle_gam_test, handle_gam_test_page, is_gam_asset_path, -}; -use trusted_server_common::gdpr::{handle_consent_request, handle_data_subject_request}; -use trusted_server_common::partners::handle_partner_asset; -use trusted_server_common::prebid::handle_prebid_test; -use trusted_server_common::prebid_proxy::{handle_prebid_auction, handle_prebid_cookie_sync}; -use trusted_server_common::privacy::handle_privacy_policy; use trusted_server_common::proxy::{ handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild, handle_first_party_proxy_sign, }; -use trusted_server_common::publisher::{ - handle_edgepubs_page, handle_main_page, handle_publisher_request, handle_tsjs_dynamic, -}; +use trusted_server_common::publisher::{handle_publisher_request, handle_tsjs_dynamic}; use trusted_server_common::settings::Settings; use trusted_server_common::settings_data::get_settings; -use trusted_server_common::why::handle_why_trusted_server; mod error; use crate::error::to_error_response; @@ -43,19 +30,6 @@ fn main(req: Request) -> Result { futures::executor::block_on(route_request(settings, req)) } -/// Routes incoming requests to appropriate handlers. -/// -/// This function implements the application's routing logic. It first checks -/// for known routes, and if none match, it proxies the request to the -/// publisher's origin server as a fallback. -/// Checks if the EdgePubs feature is enabled in experimental settings. -fn is_edgepubs_enabled(settings: &Settings) -> bool { - settings - .experimental - .as_ref() - .is_some_and(|e| e.enable_edge_pub) -} - async fn route_request(settings: Settings, req: Request) -> Result { log::info!( "FASTLY_SERVICE_VERSION: {}", @@ -65,64 +39,23 @@ async fn route_request(settings: Settings, req: Request) -> Result handle_edgepubs_page(&settings, req), - - (&Method::GET, "/auburndao", _) => handle_main_page(&settings, req), - (&Method::GET, "/ad-creative", true) => handle_ad_request(&settings, req), - // Direct asset serving for partner domains (like auburndao.com approach) - (&Method::GET, path, true) if is_partner_asset_path(path) => { - handle_partner_asset(&settings, req).await - } - // GAM asset serving (separate from Equativ, checked after Equativ) - (&Method::GET, path, true) if is_gam_asset_path(path) => { - handle_gam_asset(&settings, req).await - } - (&Method::GET, "/prebid-test", _) => handle_prebid_test(&settings, req).await, - - // Prebid Server first-party auction endpoint - (&Method::POST, "/openrtb2/auction", _) => handle_prebid_auction(&settings, req).await, - // Prebid Server first-party cookie sync - (&Method::POST, "/cookie_sync", _) => handle_prebid_cookie_sync(&settings, req).await, - - // GAM (Google Ad Manager) routes - (&Method::GET, "/gam-test", true) => handle_gam_test(&settings, req).await, - (&Method::GET, "/gam-golden-url", true) => handle_gam_golden_url(&settings, req).await, - (&Method::POST, "/gam-test-custom-url", true) => { - handle_gam_custom_url(&settings, req).await - } - (&Method::GET, "/gam-render", true) => handle_gam_render(&settings, req).await, - (&Method::GET, "/gam-test-page", true) => handle_gam_test_page(&settings, req), - // GDPR compliance routes - (&Method::GET | &Method::POST, "/gdpr/consent", _) => { - handle_consent_request(&settings, req) - } - (&Method::GET | &Method::DELETE, "/gdpr/data", _) => { - handle_data_subject_request(&settings, req) - } - - // Static content pages - (&Method::GET, "/privacy-policy", _) => handle_privacy_policy(&settings, req), - (&Method::GET, "/why-trusted-server", _) => handle_why_trusted_server(&settings, req), - + let result = match (method, path) { // Serve the tsjs library - (&Method::GET, path, _) if path.starts_with("/static/tsjs=") => { + (&Method::GET, path) if path.starts_with("/static/tsjs=") => { handle_tsjs_dynamic(&settings, req) } // tsjs endpoints - (&Method::GET, "/first-party/ad", _) => handle_server_ad_get(&settings, req).await, - (&Method::POST, "/third-party/ad", _) => handle_server_ad(&settings, req).await, - (&Method::GET, "/first-party/proxy", _) => handle_first_party_proxy(&settings, req).await, - (&Method::GET, "/first-party/click", _) => handle_first_party_click(&settings, req).await, - (&Method::GET, "/first-party/sign", _) | (&Method::POST, "/first-party/sign", _) => { + (&Method::GET, "/first-party/ad") => handle_server_ad_get(&settings, req).await, + (&Method::POST, "/third-party/ad") => handle_server_ad(&settings, req).await, + (&Method::GET, "/first-party/proxy") => handle_first_party_proxy(&settings, req).await, + (&Method::GET, "/first-party/click") => handle_first_party_click(&settings, req).await, + (&Method::GET, "/first-party/sign") | (&Method::POST, "/first-party/sign") => { handle_first_party_proxy_sign(&settings, req).await } - (&Method::POST, "/first-party/proxy-rebuild", _) => { + (&Method::POST, "/first-party/proxy-rebuild") => { handle_first_party_proxy_rebuild(&settings, req).await } @@ -147,14 +80,6 @@ async fn route_request(settings: Settings, req: Request) -> Result bool { - // Only handle Equativ/Smart AdServer assets for now - path.contains("/diff/") || // Equativ assets - path.ends_with(".png") || // Images - path.ends_with(".jpg") || // Images - path.ends_with(".gif") // Images -} - fn init_logger() { let logger = Logger::builder() .default_endpoint("tslog") diff --git a/fastly.toml b/fastly.toml index c3729c5..3a13224 100644 --- a/fastly.toml +++ b/fastly.toml @@ -16,24 +16,6 @@ build = """ # Production Backends for Partner Asset Serving (like auburndao.com) [backends] - - [backends.tpc_googlesyndication_backend] - url = "https://tpc.googlesyndication.com" - - [backends.GAM_javascript_backend] - url = "https://pagead2.googlesyndication.com" - - [backends.GTS_services_backend] - url = "https://googletagservices.com" - - [backends.pagead2_googlesyndication_backend] - url = "https://googlesyndication.com" - - [backends.equativ_sascdn_backend] - url = "https://creatives.sascdn.com" - - [backends.googleads_doubleclick_backend] - url = "https://securepubads.g.doubleclick.net" [local_server] address = "127.0.0.1:7676" @@ -42,34 +24,6 @@ build = """ [local_server.backends] - [local_server.backends.equativ_ad_api_2] - url = "https://adapi-srv-eu.smartadserver.com" - - [local_server.backends.gam_backend] - url = "https://securepubads.g.doubleclick.net" - [local_server.backends.wordpress_backend] - url = "http://localhost:8080" # Adjust this to your local WordPress URL - [local_server.backends.edgepubs_main_be] - url = "https://api.edgepubs.com" # EdgePubs API backend (update when you provide the real backend URL) - - # Partner Asset Backends (matching auburndao.com approach) - [local_server.backends.tpc_googlesyndication_backend] - url = "https://tpc.googlesyndication.com" - [local_server.backends.GAM_javascript_backend] - url = "https://pagead2.googlesyndication.com" - [local_server.backends.GTS_services_backend] - url = "https://googletagservices.com" - [local_server.backends.pagead2_googlesyndication_backend] - url = "https://googlesyndication.com" - [local_server.backends.equativ_sascdn_backend] - url = "https://creatives.sascdn.com" - [local_server.backends.googleads_doubleclick_backend] - url = "https://securepubads.g.doubleclick.net" - - # Backend for proxying ad creatives and tracking pixels - [local_server.backends.ad_cdn_backend] - url = "https://cdn.adsrvr.org" # Default, will be overridden dynamically - [local_server.kv_stores] [[local_server.kv_stores.counter_store]] key = "placeholder" diff --git a/trusted-server.toml b/trusted-server.toml index 73455f8..6fbe9d9 100644 --- a/trusted-server.toml +++ b/trusted-server.toml @@ -1,7 +1,3 @@ -[ad_server] -ad_partner_url = "equativ_ad_api_2" -sync_url = "https://adapi-srv-eu.smartadserver.com/ac?pgid=2040327&fmtid=137675&synthetic_id={{synthetic_id}}" - [publisher] domain = "test-publisher.com" cookie_domain = ".test-publisher.com" @@ -10,22 +6,11 @@ proxy_secret = "change-me-proxy-secret" [prebid] server_url = "http://68.183.113.79:8000" -account_id = "1001" timeout_ms = 1000 bidders = ["kargo", "rubicon", "appnexus", "openx"] auto_configure = false debug = false -[gam] -publisher_id = "88059007" -server_url = "https://securepubads.g.doubleclick.net/gampad/ads" -ad_units = [ - { name = "Flex8:1", size = "flexible" }, - { name = "Fixed728x90", size = "728x90" }, - { name = "Static8:1", size = "flexible" }, - { name = "Static728x90", size = "728x90" } -] - [synthetic] counter_store = "counter_store" opid_store = "opid_store" @@ -38,39 +23,3 @@ secret_key = "trusted-server" # - "publisher_domain" # - "accept_language" template = "{{ client_ip }}:{{ user_agent }}:{{ first_party_id }}:{{ auth_user_id }}:{{ publisher_domain }}:{{ accept_language }}" - -# Partner Proxy Configuration -[partners] - -[partners.gam] -enabled = true -name = "Google Ad Manager" -domains_to_proxy = [ - "securepubads.g.doubleclick.net", - "tpc.googlesyndication.com", - "googletagservices.com", - "googlesyndication.com" -] -proxy_domain = "www.edgepubs.com" -backend_name = "gam_proxy_backend" - -[partners.equativ] -enabled = true -name = "Equativ (Smart AdServer)" -domains_to_proxy = [ - "creatives.sascdn.com", - "adapi-srv-eu.smartadserver.com" -] -proxy_domain = "www.edgepubs.com" -backend_name = "equativ_proxy_backend" - -[partners.prebid] -enabled = true -name = "Prebid Server" -domains_to_proxy = ["68.183.113.79"] -proxy_domain = "www.edgepubs.com" -backend_name = "prebid_proxy_backend" - -# Experimental features -[experimental] -enable_edge_pub = false