From 34d4796eb7a3c30ac5ada01e1798ca67fdb51982 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:02:52 -0700 Subject: [PATCH 1/4] Moved handlers to separate files --- crates/common/src/advertiser.rs | 287 +++++++++++++++ crates/common/src/gdpr.rs | 56 ++- crates/common/src/geo.rs | 80 ++++ crates/common/src/lib.rs | 5 + crates/common/src/prebid.rs | 114 +++++- crates/common/src/privacy.rs | 30 ++ crates/common/src/publisher.rs | 126 +++++++ crates/common/src/why.rs | 31 ++ crates/fastly/src/main.rs | 631 ++------------------------------ 9 files changed, 756 insertions(+), 604 deletions(-) create mode 100644 crates/common/src/advertiser.rs create mode 100644 crates/common/src/geo.rs create mode 100644 crates/common/src/publisher.rs diff --git a/crates/common/src/advertiser.rs b/crates/common/src/advertiser.rs new file mode 100644 index 0000000..4ceb043 --- /dev/null +++ b/crates/common/src/advertiser.rs @@ -0,0 +1,287 @@ +//! 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_SYNTHETIC_TRUSTED_SERVER, HEADER_X_COMPRESS_HINT, HEADER_X_CONSENT_ADVERTISING, + HEADER_X_FORWARDED_FOR, +}; +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_service_version = env::var("FASTLY_SERVICE_VERSION").unwrap_or_else(|_| "unknown".to_string()); + let fastly_trace_id = + env::var("FASTLY_TRACE_ID").unwrap_or_else(|_| "unknown".to_string()); + + log::info!("Fastly Jason 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 + ); + } + } + } else { + log::warn!("Could not extract opid from impression callback URL"); + } + } else { + log::warn!("No impression callback found in ad response"); + } + } else { + log::warn!("Could not parse JSON response to extract opid"); + } + + let synthetic_header = req + .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) + .map(|h| h.to_str().unwrap_or("")); + log::info!( + "Returning response with Synthetic header: {:?}", + synthetic_header + ); + log::info!("Advertising consent: {}", advertising_consent); + + // Return the response to the client + Ok(Response::from_body(body) + .with_status(res.get_status()) + .with_header(header::CONTENT_TYPE, "application/json") + .with_header("X-Synthetic-ID", &synthetic_id) + .with_header( + "X-Consent-Advertising", + if advertising_consent { "true" } else { "false" }, + ) + .with_header("X-Fastly-PoP", &fastly_pop) + .with_header(HEADER_X_COMPRESS_HINT, "on")) + } else { + Ok(Response::from_status(res.get_status()) + .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/gdpr.rs b/crates/common/src/gdpr.rs index f7f27d9..9b2ee7b 100644 --- a/crates/common/src/gdpr.rs +++ b/crates/common/src/gdpr.rs @@ -3,13 +3,15 @@ //! 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::{Error, Request, Response}; +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. @@ -112,22 +114,41 @@ pub fn create_consent_cookie(settings: &Settings, consent: &GdprConsent) -> Stri /// /// # Errors /// -/// Returns a Fastly [`Error`] if response creation fails. -pub fn handle_consent_request(settings: &Settings, req: Request) -> Result { +/// 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(serde_json::to_string(&consent)?)) + .with_body(json_body)) } Method::POST => { // Update consent preferences - let consent: GdprConsent = serde_json::from_slice(req.into_body_bytes().as_slice())?; + 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(serde_json::to_string(&consent)?); + .with_body(json_body); response.set_header( header::SET_COOKIE, @@ -152,8 +173,13 @@ pub fn handle_consent_request(settings: &Settings, req: Request) -> Result Result { +/// 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 @@ -163,11 +189,21 @@ pub fn handle_data_subject_request(_settings: &Settings, req: Request) -> Result // TODO: Implement actual data retrieval from KV store // For now, return empty user data - data.insert(synthetic_id.to_str()?.to_string(), UserData::default()); + 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(serde_json::to_string(&data)?)) + .with_body(json_body)) } else { Ok(Response::from_status(StatusCode::BAD_REQUEST).with_body("Missing subject ID")) } diff --git a/crates/common/src/geo.rs b/crates/common/src/geo.rs new file mode 100644 index 0000000..1f12885 --- /dev/null +++ b/crates/common/src/geo.rs @@ -0,0 +1,80 @@ +//! Geographic location utilities for the trusted server. +//! +//! This module provides functions for extracting and handling geographic +//! information from incoming requests, particularly DMA (Designated Market Area) codes. + +use fastly::geo::geo_lookup; +use fastly::Request; + +use crate::constants::{ + 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, +}; + +/// Extracts the DMA (Designated Market Area) code from the request's geolocation data. +/// +/// This function: +/// 1. Checks if running in Fastly environment +/// 2. Performs geo lookup based on client IP +/// 3. Sets various geo headers on the request +/// 4. Returns the metro code (DMA) if available +/// +/// # Arguments +/// +/// * `req` - The request to extract DMA code from +/// +/// # Returns +/// +/// The DMA/metro code as a string if available, None otherwise +pub fn get_dma_code(req: &mut Request) -> Option { + // Debug: Check if we're running in Fastly environment + log::info!("Fastly Environment Check:"); + log::info!( + " FASTLY_POP: {}", + std::env::var("FASTLY_POP").unwrap_or_else(|_| "not in Fastly".to_string()) + ); + log::info!( + " FASTLY_REGION: {}", + std::env::var("FASTLY_REGION").unwrap_or_else(|_| "not in Fastly".to_string()) + ); + + // Get detailed geo information using geo_lookup + if let Some(geo) = req.get_client_ip_addr().and_then(geo_lookup) { + log::info!("Geo Information Found:"); + + // Set all available geo information in headers + let city = geo.city(); + req.set_header(HEADER_X_GEO_CITY, city); + log::info!(" City: {}", city); + + let country = geo.country_code(); + req.set_header(HEADER_X_GEO_COUNTRY, country); + log::info!(" Country: {}", country); + + req.set_header(HEADER_X_GEO_CONTINENT, format!("{:?}", geo.continent())); + log::info!(" Continent: {:?}", geo.continent()); + + req.set_header( + HEADER_X_GEO_COORDINATES, + format!("{},{}", geo.latitude(), geo.longitude()), + ); + log::info!(" Location: ({}, {})", geo.latitude(), geo.longitude()); + + // Get and set the metro code (DMA) + let metro_code = geo.metro_code(); + req.set_header(HEADER_X_GEO_METRO_CODE, metro_code.to_string()); + log::info!("Found DMA/Metro code: {}", metro_code); + return Some(metro_code.to_string()); + } else { + log::info!("No geo information available for the request"); + req.set_header(HEADER_X_GEO_INFO_AVAILABLE, "false"); + } + + // If no metro code is found, log all request headers for debugging + log::info!("No DMA/Metro code found. All request headers:"); + for (name, value) in req.get_headers() { + log::info!(" {}: {:?}", name, value); + } + + None +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index d425c2c..07fe80f 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -5,10 +5,12 @@ //! //! # Modules //! +//! - [`advertiser`]: Ad serving and advertiser integration functionality //! - [`constants`]: Application-wide constants and configuration values //! - [`cookies`]: Cookie parsing and generation utilities //! - [`error`]: Error types and error handling utilities //! - [`gdpr`]: GDPR consent management and TCF string parsing +//! - [`geo`]: Geographic location utilities and DMA code extraction //! - [`models`]: Data models for ad serving and callbacks //! - [`prebid`]: Prebid integration and real-time bidding support //! - [`privacy`]: Privacy utilities and helpers @@ -18,13 +20,16 @@ //! - [`test_support`]: Testing utilities and mocks //! - [`why`]: Debugging and introspection utilities +pub mod advertiser; pub mod constants; pub mod cookies; pub mod error; pub mod gdpr; +pub mod geo; pub mod models; pub mod prebid; pub mod privacy; +pub mod publisher; pub mod settings; pub mod synthetic; pub mod templates; diff --git a/crates/common/src/prebid.rs b/crates/common/src/prebid.rs index b998986..ec129c0 100644 --- a/crates/common/src/prebid.rs +++ b/crates/common/src/prebid.rs @@ -4,16 +4,17 @@ //! to enable header bidding and real-time ad auctions. use error_stack::Report; -use fastly::http::{header, Method}; +use fastly::http::{header, Method, StatusCode}; use fastly::{Error, Request, Response}; use serde_json::json; use crate::constants::{ - HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_FORWARDED_FOR, + 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; +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 { @@ -198,6 +199,113 @@ impl PrebidRequest { } } +/// 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::*; diff --git a/crates/common/src/privacy.rs b/crates/common/src/privacy.rs index e34251b..4a25fcc 100644 --- a/crates/common/src/privacy.rs +++ b/crates/common/src/privacy.rs @@ -1,3 +1,33 @@ +//! 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#" diff --git a/crates/common/src/publisher.rs b/crates/common/src/publisher.rs new file mode 100644 index 0000000..8c6b02b --- /dev/null +++ b/crates/common/src/publisher.rs @@ -0,0 +1,126 @@ +use error_stack::Report; +use fastly::http::{header, StatusCode}; +use fastly::{Request, Response}; + +use crate::constants::{ + HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, 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::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::synthetic::{generate_synthetic_id, get_or_generate_synthetic_id}; +use crate::templates::HTML_TEMPLATE; + +/// 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) +} diff --git a/crates/common/src/why.rs b/crates/common/src/why.rs index 40c1246..0015922 100644 --- a/crates/common/src/why.rs +++ b/crates/common/src/why.rs @@ -1,3 +1,34 @@ +//! 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#" diff --git a/crates/fastly/src/main.rs b/crates/fastly/src/main.rs index e32d593..7295b5e 100644 --- a/crates/fastly/src/main.rs +++ b/crates/fastly/src/main.rs @@ -1,36 +1,23 @@ -use std::env; - -use fastly::geo::geo_lookup; use fastly::http::{header, Method, StatusCode}; -use fastly::KVStore; use fastly::{Error, Request, Response}; use log::LevelFilter::Info; -use serde_json::json; mod error; use crate::error::to_error_response; -use trusted_server_common::constants::{ - HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, 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 trusted_server_common::cookies::create_synthetic_cookie; -// Note: TrustedServerError is used internally by the common crate -use trusted_server_common::gdpr::{ - get_consent_from_request, handle_consent_request, handle_data_subject_request, GdprConsent, -}; -use trusted_server_common::models::AdResponse; -use trusted_server_common::prebid::PrebidRequest; -use trusted_server_common::privacy::PRIVACY_TEMPLATE; +use trusted_server_common::advertiser::handle_ad_request; +use trusted_server_common::constants::HEADER_X_COMPRESS_HINT; +use trusted_server_common::gdpr::{handle_consent_request, handle_data_subject_request}; +use trusted_server_common::prebid::handle_prebid_test; +use trusted_server_common::privacy::handle_privacy_policy; +use trusted_server_common::publisher::handle_main_page; use trusted_server_common::settings::Settings; -use trusted_server_common::synthetic::{generate_synthetic_id, get_or_generate_synthetic_id}; -use trusted_server_common::templates::HTML_TEMPLATE; -use trusted_server_common::why::WHY_TEMPLATE; +use trusted_server_common::why::handle_why_trusted_server; #[fastly::main] fn main(req: Request) -> Result { + log_fastly::init_simple("mylogs", Info); + let settings = match Settings::new() { Ok(s) => s, Err(e) => { @@ -40,583 +27,45 @@ fn main(req: Request) -> Result { }; log::info!("Settings {settings:?}"); - futures::executor::block_on(async { - log::info!( - "FASTLY_SERVICE_VERSION: {}", - std::env::var("FASTLY_SERVICE_VERSION").unwrap_or_else(|_| String::new()) - ); - - match (req.get_method(), req.get_path()) { - (&Method::GET, "/") => handle_main_page(&settings, req), - (&Method::GET, "/ad-creative") => handle_ad_request(&settings, req), - (&Method::GET, "/prebid-test") => handle_prebid_test(&settings, req).await, - (&Method::GET, "/gdpr/consent") => handle_consent_request(&settings, req), - (&Method::POST, "/gdpr/consent") => handle_consent_request(&settings, req), - (&Method::GET, "/gdpr/data") => handle_data_subject_request(&settings, req), - (&Method::DELETE, "/gdpr/data") => handle_data_subject_request(&settings, req), - (&Method::GET, "/privacy-policy") => Ok(Response::from_status(StatusCode::OK) - .with_body(PRIVACY_TEMPLATE) - .with_header(header::CONTENT_TYPE, "text/html") - .with_header(HEADER_X_COMPRESS_HINT, "on")), - (&Method::GET, "/why-trusted-server") => Ok(Response::from_status(StatusCode::OK) - .with_body(WHY_TEMPLATE) - .with_header(header::CONTENT_TYPE, "text/html") - .with_header(HEADER_X_COMPRESS_HINT, "on")), - _ => Ok(Response::from_status(StatusCode::NOT_FOUND) - .with_body("Not Found") - .with_header(header::CONTENT_TYPE, "text/plain") - .with_header(HEADER_X_COMPRESS_HINT, "on")), - } - }) -} - -fn get_dma_code(req: &mut Request) -> Option { - // Debug: Check if we're running in Fastly environment - log::info!("Fastly Environment Check:"); - log::info!( - " FASTLY_POP: {}", - std::env::var("FASTLY_POP").unwrap_or_else(|_| "not in Fastly".to_string()) - ); - log::info!( - " FASTLY_REGION: {}", - std::env::var("FASTLY_REGION").unwrap_or_else(|_| "not in Fastly".to_string()) - ); - - // Get detailed geo information using geo_lookup - if let Some(geo) = req.get_client_ip_addr().and_then(geo_lookup) { - log::info!("Geo Information Found:"); - - // Set all available geo information in headers - let city = geo.city(); - req.set_header(HEADER_X_GEO_CITY, city); - log::info!(" City: {}", city); - - let country = geo.country_code(); - req.set_header(HEADER_X_GEO_COUNTRY, country); - log::info!(" Country: {}", country); - - req.set_header(HEADER_X_GEO_CONTINENT, format!("{:?}", geo.continent())); - log::info!(" Continent: {:?}", geo.continent()); - - req.set_header( - HEADER_X_GEO_COORDINATES, - format!("{},{}", geo.latitude(), geo.longitude()), - ); - log::info!(" Location: ({}, {})", geo.latitude(), geo.longitude()); - - // Get and set the metro code (DMA) - let metro_code = geo.metro_code(); - req.set_header(HEADER_X_GEO_METRO_CODE, metro_code.to_string()); - log::info!("Found DMA/Metro code: {}", metro_code); - return Some(metro_code.to_string()); - } else { - log::info!("No geo information available for the request"); - req.set_header(HEADER_X_GEO_INFO_AVAILABLE, "false"); - } - - // If no metro code is found, log all request headers for debugging - log::info!("No DMA/Metro code found. All request headers:"); - for (name, value) in req.get_headers() { - log::info!(" {}: {:?}", name, value); - } - - None + futures::executor::block_on(route_request(settings, req)) } -/// Handles the main page request. -/// -/// Serves the main page with synthetic ID generation and ad integration. +/// Routes incoming requests to appropriate handlers. /// -/// # Errors -/// -/// Returns a Fastly [`Error`] if response creation fails. -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, - ); - - log_fastly::init_simple("mylogs", Info); - - // 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 = match generate_synthetic_id(settings, &req) { - Ok(id) => id, - Err(e) => return Ok(to_error_response(e)), - }; - - // 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 = match get_or_generate_synthetic_id(settings, &req) { - Ok(id) => id, - Err(e) => return Ok(to_error_response(e)), - }; - +/// This function implements the application's routing logic, matching HTTP methods +/// and paths to their corresponding handler functions. +async fn route_request(settings: Settings, req: Request) -> Result { log::info!( - "Existing Trusted Server header: {:?}", - req.get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) + "FASTLY_SERVICE_VERSION: {}", + ::std::env::var("FASTLY_SERVICE_VERSION").unwrap_or_else(|_| String::new()) ); - 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 &[ - "X-Geo-City", - "X-Geo-Country", - "X-Geo-Continent", - "X-Geo-Coordinates", - "X-Geo-Metro-Code", - "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) -} - -/// Handles ad creative requests. -/// -/// Processes ad requests with synthetic ID and consent checking. -/// -/// # Errors -/// -/// Returns a Fastly [`Error`] if response creation fails. -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 { - match generate_synthetic_id(settings, &req) { - Ok(id) => id, - Err(e) => return Ok(to_error_response(e)), - } - } else { - // Use a generic ID for non-personalized ads - "non-personalized".to_string() + let result = match (req.get_method(), req.get_path()) { + // Main application routes + (&Method::GET, "/") => handle_main_page(&settings, req), + (&Method::GET, "/ad-creative") => handle_ad_request(&settings, req), + (&Method::GET, "/prebid-test") => handle_prebid_test(&settings, req).await, + + // 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), + + // Catch-all 404 handler + _ => return Ok(not_found_response()), }; - // 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_service_version = env::var("FASTLY_SERVICE_VERSION").unwrap_or_else(|_| "unknown".to_string()); - let fastly_trace_id = - env::var("FASTLY_TRACE_ID").unwrap_or_else(|_| "unknown".to_string()); - - log::info!("Fastly Jason 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); - } - } - - // Attach PoP info to the response - //response.set_header("X-Debug-Fastly-PoP", &fastly_pop); - //log::info!("Added X-Debug-Fastly-PoP: {}", fastly_pop); - - 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("{}")) - } - } + // Convert any errors to HTTP error responses + result.map_or_else(|e| Ok(to_error_response(e)), Ok) } -/// Handles the prebid test route with detailed error logging -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 { - match ( - generate_synthetic_id(settings, &req), - get_or_generate_synthetic_id(settings, &req), - ) { - (Ok(fresh), Ok(synth)) => (fresh, synth), - (Err(e), _) | (_, Err(e)) => { - log::error!("Failed to generate IDs: {:?}", e); - return Ok(Response::from_status(StatusCode::INTERNAL_SERVER_ERROR) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "Failed to generate IDs", - "details": format!("{:?}", e) - }))?); - } - } - } 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 = match PrebidRequest::new(settings, &req) { - Ok(req) => { - log::info!( - "Successfully created PrebidRequest with synthetic ID: {}", - req.synthetic_id - ); - req - } - Err(e) => { - log::error!("Error creating PrebidRequest: {:?}", e); - return Ok(Response::from_status(StatusCode::INTERNAL_SERVER_ERROR) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "Failed to create prebid request", - "details": format!("{:?}", e) - }))?); - } - }; - - 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"); - Ok(Response::from_status(StatusCode::INTERNAL_SERVER_ERROR) - .with_header(header::CONTENT_TYPE, "application/json") - .with_body_json(&json!({ - "error": "Failed to send bid request", - "details": format!("{:?}", e), - "backend": "prebid_backend" - }))?) - } - } +/// Creates a standard 404 Not Found response. +fn not_found_response() -> Response { + Response::from_status(StatusCode::NOT_FOUND) + .with_body("Not Found") + .with_header(header::CONTENT_TYPE, "text/plain") + .with_header(HEADER_X_COMPRESS_HINT, "on") } From 887000487f8b6a0c50f43c6d5b893ebb0784f6cf Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:39:02 -0700 Subject: [PATCH 2/4] Fixed formatting --- crates/common/src/gdpr.rs | 35 +++++++++++++++++++---------------- crates/common/src/prebid.rs | 2 +- crates/fastly/src/main.rs | 10 ++++++---- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/crates/common/src/gdpr.rs b/crates/common/src/gdpr.rs index 9b2ee7b..4a72653 100644 --- a/crates/common/src/gdpr.rs +++ b/crates/common/src/gdpr.rs @@ -125,11 +125,12 @@ pub fn handle_consent_request( 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 { + 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)) @@ -140,12 +141,13 @@ pub fn handle_consent_request( .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 { + + 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); @@ -189,17 +191,18 @@ pub fn handle_data_subject_request( // 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 { + 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 { + 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") diff --git a/crates/common/src/prebid.rs b/crates/common/src/prebid.rs index ec129c0..19b5e8a 100644 --- a/crates/common/src/prebid.rs +++ b/crates/common/src/prebid.rs @@ -297,7 +297,7 @@ pub async fn handle_prebid_test( 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), diff --git a/crates/fastly/src/main.rs b/crates/fastly/src/main.rs index 7295b5e..5dc630b 100644 --- a/crates/fastly/src/main.rs +++ b/crates/fastly/src/main.rs @@ -45,15 +45,17 @@ async fn route_request(settings: Settings, req: Request) -> Result handle_main_page(&settings, req), (&Method::GET, "/ad-creative") => handle_ad_request(&settings, req), (&Method::GET, "/prebid-test") => handle_prebid_test(&settings, req).await, - + // 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), - + (&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), - + // Catch-all 404 handler _ => return Ok(not_found_response()), }; From 9fcb0e0031f92db25d30ad8226c9808dca259bba Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:02:39 -0700 Subject: [PATCH 3/4] Initalize logger with formatting --- Cargo.lock | 11 +++++++++++ crates/fastly/Cargo.toml | 2 ++ crates/fastly/src/main.rs | 26 ++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c929cb4..2e066e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -499,6 +499,15 @@ dependencies = [ "wit-bindgen-rt 0.42.1", ] +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1604,8 +1613,10 @@ dependencies = [ name = "trusted-server-fastly" version = "0.1.0" dependencies = [ + "chrono", "error-stack", "fastly", + "fern", "futures", "log", "log-fastly", diff --git a/crates/fastly/Cargo.toml b/crates/fastly/Cargo.toml index d98e2b5..618bd06 100644 --- a/crates/fastly/Cargo.toml +++ b/crates/fastly/Cargo.toml @@ -4,8 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] +chrono = "0.4.41" error-stack = "0.5" fastly = "0.11.5" +fern = "0.7.1" futures = "0.3" log = "0.4.20" log-fastly = "0.11.5" diff --git a/crates/fastly/src/main.rs b/crates/fastly/src/main.rs index 5dc630b..705344f 100644 --- a/crates/fastly/src/main.rs +++ b/crates/fastly/src/main.rs @@ -1,6 +1,6 @@ use fastly::http::{header, Method, StatusCode}; use fastly::{Error, Request, Response}; -use log::LevelFilter::Info; +use log_fastly::Logger; mod error; use crate::error::to_error_response; @@ -16,7 +16,7 @@ use trusted_server_common::why::handle_why_trusted_server; #[fastly::main] fn main(req: Request) -> Result { - log_fastly::init_simple("mylogs", Info); + init_logger(); let settings = match Settings::new() { Ok(s) => s, @@ -71,3 +71,25 @@ fn not_found_response() -> Response { .with_header(header::CONTENT_TYPE, "text/plain") .with_header(HEADER_X_COMPRESS_HINT, "on") } + +fn init_logger() { + let logger = Logger::builder() + .default_endpoint("tslog") + // .echo_stdout(true) + .max_level(log::LevelFilter::Debug) + .build() + .expect("Failed to build Logger"); + + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} {} {}", + chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), + record.level(), + message + )) + }) + .chain(Box::new(logger) as Box) + .apply() + .expect("Failed to initialize logger"); +} From b589d4112141199c485a2b9af3834347c8985f87 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:52:16 -0700 Subject: [PATCH 4/4] Output logs to stdout --- crates/fastly/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fastly/src/main.rs b/crates/fastly/src/main.rs index ee993de..1e71749 100644 --- a/crates/fastly/src/main.rs +++ b/crates/fastly/src/main.rs @@ -87,7 +87,7 @@ fn not_found_response() -> Response { fn init_logger() { let logger = Logger::builder() .default_endpoint("tslog") - // .echo_stdout(true) + .echo_stdout(true) .max_level(log::LevelFilter::Debug) .build() .expect("Failed to build Logger");