diff --git a/CHANGELOG.md b/CHANGELOG.md index f02c460..b10b256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Upgrade to rust 1.87.0 - Upgrade to fastly-cli 11.3.0 +- Changed to use constants for headers ## [1.0.6] - 2025-05-29 diff --git a/Cargo.lock b/Cargo.lock index 847d993..9fbd6da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -715,9 +715,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1590,6 +1590,7 @@ dependencies = [ "handlebars", "hex", "hmac", + "http", "log", "log-fastly", "serde", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 42b84be..fc592c8 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -17,6 +17,7 @@ futures = "0.3" handlebars = "6.3.2" hex = "0.4.3" hmac = "0.12.1" +http = "1.3.1" log = "0.4.20" log-fastly = "0.10.0" serde = { version = "1.0", features = ["derive"] } diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index de5edba..02ed380 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -1,3 +1,21 @@ -pub const SYNTHETIC_HEADER_FRESH: &str = "X-Synthetic-Fresh"; -pub const SYNTHETIC_HEADER_TRUSTED_SERVER: &str = "X-Synthetic-Trusted-Server"; -pub const SYNTHETIC_HEADER_PUB_USER_ID: &str = "X-Pub-User-ID"; +use http::header::HeaderName; + +pub const HEADER_SYNTHETIC_FRESH: HeaderName = HeaderName::from_static("x-synthetic-fresh"); +pub const HEADER_SYNTHETIC_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id"); +pub const HEADER_X_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id"); +pub const HEADER_SYNTHETIC_TRUSTED_SERVER: HeaderName = + HeaderName::from_static("x-synthetic-trusted-server"); +pub const HEADER_X_CONSENT_ADVERTISING: HeaderName = + HeaderName::from_static("x-consent-advertising"); +pub const HEADER_X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for"); +pub const HEADER_X_GEO_CITY: HeaderName = HeaderName::from_static("x-geo-city"); +pub const HEADER_X_GEO_CONTINENT: HeaderName = HeaderName::from_static("x-geo-continent"); +pub const HEADER_X_GEO_COORDINATES: HeaderName = HeaderName::from_static("x-geo-coordinates"); +pub const HEADER_X_GEO_COUNTRY: HeaderName = HeaderName::from_static("x-geo-country"); +pub const HEADER_X_GEO_INFO_AVAILABLE: HeaderName = HeaderName::from_static("x-geo-info-available"); +pub const HEADER_X_GEO_METRO_CODE: HeaderName = HeaderName::from_static("x-geo-metro-code"); +pub const HEADER_X_GEO_REGION: HeaderName = HeaderName::from_static("x-geo-region"); +pub const HEADER_X_SUBJECT_ID: HeaderName = HeaderName::from_static("x-subject-id"); +pub const HEADER_X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id"); +pub const HEADER_X_COMPRESS_HINT: HeaderName = HeaderName::from_static("x-compress-hint"); +pub const HEADER_X_DEBUG_FASTLY_POP: HeaderName = HeaderName::from_static("x-debug-fastly-pop"); diff --git a/crates/common/src/gdpr.rs b/crates/common/src/gdpr.rs index adf36bc..86e53fc 100644 --- a/crates/common/src/gdpr.rs +++ b/crates/common/src/gdpr.rs @@ -1,10 +1,12 @@ -use crate::cookies; -use crate::settings::Settings; use fastly::http::{header, Method, StatusCode}; use fastly::{Error, Request, Response}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use crate::constants::HEADER_X_SUBJECT_ID; +use crate::cookies; +use crate::settings::Settings; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GdprConsent { pub analytics: bool, @@ -93,7 +95,7 @@ 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("X-Subject-ID") { + 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(); @@ -110,7 +112,7 @@ pub fn handle_data_subject_request(_settings: &Settings, req: Request) -> Result } Method::DELETE => { // Handle right to erasure (right to be forgotten) - if let Some(_synthetic_id) = req.get_header("X-Subject-ID") { + 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")) @@ -319,7 +321,7 @@ mod tests { 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("X-Subject-ID", "test-subject-123"); + 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); @@ -348,7 +350,7 @@ mod tests { 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("X-Subject-ID", "test-subject-123"); + 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); diff --git a/crates/common/src/prebid.rs b/crates/common/src/prebid.rs index 7db2d5f..8802b74 100644 --- a/crates/common/src/prebid.rs +++ b/crates/common/src/prebid.rs @@ -2,7 +2,9 @@ use fastly::http::{header, Method}; use fastly::{Error, Request, Response}; use serde_json::json; -use crate::constants::{SYNTHETIC_HEADER_FRESH, SYNTHETIC_HEADER_TRUSTED_SERVER}; +use crate::constants::{ + HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_FORWARDED_FOR, +}; use crate::settings::Settings; use crate::synthetic::generate_synthetic_id; @@ -31,7 +33,7 @@ impl PrebidRequest { 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 = req - .get_header(SYNTHETIC_HEADER_TRUSTED_SERVER) + .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()) .unwrap_or_else(|| generate_synthetic_id(settings, req)); @@ -41,7 +43,7 @@ impl PrebidRequest { .get_client_ip_addr() .map(|ip| ip.to_string()) .unwrap_or_else(|| { - req.get_header("X-Forwarded-For") + 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 @@ -98,7 +100,7 @@ impl PrebidRequest { // Get and store the POTSI ID value from the incoming request let id: String = incoming_req - .get_header(SYNTHETIC_HEADER_TRUSTED_SERVER) + .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()); @@ -167,10 +169,10 @@ impl PrebidRequest { }); req.set_header(header::CONTENT_TYPE, "application/json"); - req.set_header("X-Forwarded-For", &self.client_ip); + req.set_header(HEADER_X_FORWARDED_FOR, &self.client_ip); req.set_header(header::ORIGIN, &self.origin); - req.set_header(SYNTHETIC_HEADER_FRESH, &self.synthetic_id); - req.set_header(SYNTHETIC_HEADER_TRUSTED_SERVER, &id); + req.set_header(HEADER_SYNTHETIC_FRESH, &self.synthetic_id); + req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, &id); println!( "Sending prebid request with Fresh ID: {} and Trusted Server ID: {}", @@ -211,10 +213,10 @@ mod tests { 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(SYNTHETIC_HEADER_TRUSTED_SERVER, "existing-synthetic-id"); + 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("X-Forwarded-For", "192.168.1.1, 10.0.0.1"); + req.set_header(HEADER_X_FORWARDED_FOR, "192.168.1.1, 10.0.0.1"); let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); @@ -280,7 +282,7 @@ mod tests { fn test_prebid_request_x_forwarded_for_parsing() { let settings = create_test_settings(); let mut req = Request::get("https://example.com/test"); - req.set_header("X-Forwarded-For", "192.168.1.1, 10.0.0.1, 172.16.0.1"); + 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(); @@ -330,7 +332,7 @@ mod tests { // Test with empty X-Forwarded-For let mut req = Request::get("https://example.com/test"); - req.set_header("X-Forwarded-For", ""); + 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()); diff --git a/crates/common/src/synthetic.rs b/crates/common/src/synthetic.rs index a22610c..ebabb11 100644 --- a/crates/common/src/synthetic.rs +++ b/crates/common/src/synthetic.rs @@ -5,7 +5,7 @@ use hmac::{Hmac, Mac}; use serde_json::json; use sha2::Sha256; -use crate::constants::{SYNTHETIC_HEADER_PUB_USER_ID, SYNTHETIC_HEADER_TRUSTED_SERVER}; +use crate::constants::{HEADER_SYNTHETIC_PUB_USER_ID, HEADER_SYNTHETIC_TRUSTED_SERVER}; use crate::cookies::handle_request_cookies; use crate::settings::Settings; @@ -21,7 +21,7 @@ pub fn generate_synthetic_id(settings: &Settings, req: &Request) -> String { .map(|cookie| cookie.value().to_string()) }); let auth_user_id = req - .get_header(SYNTHETIC_HEADER_PUB_USER_ID) + .get_header(HEADER_SYNTHETIC_PUB_USER_ID) .map(|h| h.to_str().unwrap_or("anonymous")); let publisher_domain = req .get_header(header::HOST) @@ -61,7 +61,7 @@ pub fn generate_synthetic_id(settings: &Settings, req: &Request) -> String { pub fn get_or_generate_synthetic_id(settings: &Settings, req: &Request) -> String { // First try to get existing Trusted Server ID from header if let Some(synthetic_id) = req - .get_header(SYNTHETIC_HEADER_TRUSTED_SERVER) + .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()) { @@ -96,9 +96,10 @@ pub fn get_or_generate_synthetic_id(settings: &Settings, req: &Request) -> Strin #[cfg(test)] mod tests { use super::*; - use fastly::http::HeaderValue; + use crate::constants::HEADER_X_PUB_USER_ID; + use fastly::http::{HeaderName, HeaderValue}; - fn create_test_request(headers: Vec<(&str, &str)>) -> Request { + fn create_test_request(headers: Vec<(HeaderName, &str)>) -> Request { let mut req = Request::new("GET", "http://example.com"); for (key, value) in headers { req.set_header(key, HeaderValue::from_str(value).unwrap()); @@ -129,11 +130,11 @@ mod tests { fn test_generate_synthetic_id() { let settings: Settings = create_settings(); let req = create_test_request(vec![ - (header::USER_AGENT.as_ref(), "Mozilla/5.0"), - (header::COOKIE.as_ref(), "pub_userid=12345"), - ("X-Pub-User-ID", "67890"), - (header::HOST.as_ref(), "example.com"), - (header::ACCEPT_LANGUAGE.as_ref(), "en-US,en;q=0.9"), + (header::USER_AGENT, "Mozilla/5.0"), + (header::COOKIE, "pub_userid=12345"), + (HEADER_X_PUB_USER_ID, "67890"), + (header::HOST, "example.com"), + (header::ACCEPT_LANGUAGE, "en-US,en;q=0.9"), ]); let synthetic_id = generate_synthetic_id(&settings, &req); @@ -148,7 +149,7 @@ mod tests { fn test_get_or_generate_synthetic_id_with_header() { let settings = create_settings(); let req = create_test_request(vec![( - SYNTHETIC_HEADER_TRUSTED_SERVER, + HEADER_SYNTHETIC_TRUSTED_SERVER, "existing_synthetic_id", )]); @@ -159,10 +160,7 @@ mod tests { #[test] fn test_get_or_generate_synthetic_id_with_cookie() { let settings = create_settings(); - let req = create_test_request(vec![( - header::COOKIE.as_ref(), - "synthetic_id=existing_cookie_id", - )]); + let req = create_test_request(vec![(header::COOKIE, "synthetic_id=existing_cookie_id")]); let synthetic_id = get_or_generate_synthetic_id(&settings, &req); assert_eq!(synthetic_id, "existing_cookie_id"); diff --git a/crates/fastly/src/main.rs b/crates/fastly/src/main.rs index bd7dc84..8713198 100644 --- a/crates/fastly/src/main.rs +++ b/crates/fastly/src/main.rs @@ -6,7 +6,12 @@ use log::LevelFilter::Info; use serde_json::json; use std::env; -use trusted_server_common::constants::{SYNTHETIC_HEADER_FRESH, SYNTHETIC_HEADER_TRUSTED_SERVER}; +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; use trusted_server_common::gdpr::{ get_consent_from_request, handle_consent_request, handle_data_subject_request, @@ -41,15 +46,15 @@ fn main(req: Request) -> Result { (&Method::GET, "/privacy-policy") => Ok(Response::from_status(StatusCode::OK) .with_body(PRIVACY_TEMPLATE) .with_header(header::CONTENT_TYPE, "text/html") - .with_header("x-compress-hint", "on")), + .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("x-compress-hint", "on")), + .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("x-compress-hint", "on")), + .with_header(HEADER_X_COMPRESS_HINT, "on")), } }) } @@ -72,30 +77,30 @@ fn get_dma_code(req: &mut Request) -> Option { // Set all available geo information in headers let city = geo.city(); - req.set_header("X-Geo-City", city); + req.set_header(HEADER_X_GEO_CITY, city); println!(" City: {}", city); let country = geo.country_code(); - req.set_header("X-Geo-Country", country); + req.set_header(HEADER_X_GEO_COUNTRY, country); println!(" Country: {}", country); - req.set_header("X-Geo-Continent", format!("{:?}", geo.continent())); + req.set_header(HEADER_X_GEO_CONTINENT, format!("{:?}", geo.continent())); println!(" Continent: {:?}", geo.continent()); req.set_header( - "X-Geo-Coordinates", + HEADER_X_GEO_COORDINATES, format!("{},{}", geo.latitude(), geo.longitude()), ); println!(" Location: ({}, {})", geo.latitude(), geo.longitude()); // Get and set the metro code (DMA) let metro_code = geo.metro_code(); - req.set_header("X-Geo-Metro-Code", metro_code.to_string()); + req.set_header(HEADER_X_GEO_METRO_CODE, metro_code.to_string()); println!("Found DMA/Metro code: {}", metro_code); return Some(metro_code.to_string()); } else { println!("No geo information available for the request"); - req.set_header("X-Geo-Info-Available", "false"); + req.set_header(HEADER_X_GEO_INFO_AVAILABLE, "false"); } // If no metro code is found, log all request headers for debugging @@ -142,7 +147,7 @@ fn handle_main_page(settings: &Settings, mut req: Request) -> Result Result Result Result Result Result Result Result Result Result Result Result {