Skip to content

Commit 34d4796

Browse files
committed
Moved handlers to separate files
1 parent 55dac6b commit 34d4796

File tree

9 files changed

+756
-604
lines changed

9 files changed

+756
-604
lines changed

crates/common/src/advertiser.rs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
//! Ad serving and advertiser integration functionality.
2+
//!
3+
//! This module handles ad requests, including GDPR consent checking,
4+
//! synthetic ID generation, visitor tracking, and communication with
5+
//! external ad partners.
6+
7+
use std::env;
8+
9+
use error_stack::Report;
10+
use fastly::http::{header, StatusCode};
11+
use fastly::{KVStore, Request, Response};
12+
13+
use crate::constants::{
14+
HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_COMPRESS_HINT, HEADER_X_CONSENT_ADVERTISING,
15+
HEADER_X_FORWARDED_FOR,
16+
};
17+
use crate::error::TrustedServerError;
18+
use crate::gdpr::{get_consent_from_request, GdprConsent};
19+
use crate::geo::get_dma_code;
20+
use crate::models::AdResponse;
21+
use crate::settings::Settings;
22+
use crate::synthetic::generate_synthetic_id;
23+
24+
/// Handles ad creative requests.
25+
///
26+
/// Processes ad requests with synthetic ID and consent checking.
27+
///
28+
/// # Errors
29+
///
30+
/// Returns a [`TrustedServerError`] if:
31+
/// - Synthetic ID generation fails
32+
/// - Backend communication fails
33+
/// - Response creation fails
34+
pub fn handle_ad_request(
35+
settings: &Settings,
36+
mut req: Request,
37+
) -> Result<Response, Report<TrustedServerError>> {
38+
// Check GDPR consent to determine if we should serve personalized or non-personalized ads
39+
let _consent = match get_consent_from_request(&req) {
40+
Some(c) => c,
41+
None => {
42+
log::debug!("No GDPR consent found in ad request, using default");
43+
GdprConsent::default()
44+
}
45+
};
46+
let advertising_consent = req
47+
.get_header(HEADER_X_CONSENT_ADVERTISING)
48+
.and_then(|h| h.to_str().ok())
49+
.map(|v| v == "true")
50+
.unwrap_or(false);
51+
52+
// Add DMA code extraction
53+
let dma_code = get_dma_code(&mut req);
54+
55+
log::info!("Client location - DMA Code: {:?}", dma_code);
56+
57+
// Log headers for debugging
58+
let client_ip = req
59+
.get_client_ip_addr()
60+
.map(|ip| ip.to_string())
61+
.unwrap_or_else(|| "Unknown".to_string());
62+
let x_forwarded_for = req
63+
.get_header(HEADER_X_FORWARDED_FOR)
64+
.map(|h| h.to_str().unwrap_or("Unknown"));
65+
66+
log::info!("Client IP: {}", client_ip);
67+
log::info!("X-Forwarded-For: {}", x_forwarded_for.unwrap_or("None"));
68+
log::info!("Advertising consent: {}", advertising_consent);
69+
70+
// Generate synthetic ID only if we have consent
71+
let synthetic_id = if advertising_consent {
72+
generate_synthetic_id(settings, &req)?
73+
} else {
74+
// Use a generic ID for non-personalized ads
75+
"non-personalized".to_string()
76+
};
77+
78+
// Only track visits if we have consent
79+
if advertising_consent {
80+
// Increment visit counter in KV store
81+
log::info!("Opening KV store: {}", settings.synthetic.counter_store);
82+
if let Ok(Some(store)) = KVStore::open(settings.synthetic.counter_store.as_str()) {
83+
log::info!("Fetching current count for synthetic ID: {}", synthetic_id);
84+
let current_count: i32 = store
85+
.lookup(&synthetic_id)
86+
.map(|mut val| match String::from_utf8(val.take_body_bytes()) {
87+
Ok(s) => {
88+
log::info!("Value from KV store: {}", s);
89+
Some(s)
90+
}
91+
Err(e) => {
92+
log::error!("Error converting bytes to string: {}", e);
93+
None
94+
}
95+
})
96+
.map(|opt_s| {
97+
log::info!("Parsing string value: {:?}", opt_s);
98+
opt_s.and_then(|s| s.parse().ok())
99+
})
100+
.unwrap_or_else(|_| {
101+
log::info!("No existing count found, starting at 0");
102+
None
103+
})
104+
.unwrap_or(0);
105+
106+
let new_count = current_count + 1;
107+
log::info!("Incrementing count from {} to {}", current_count, new_count);
108+
109+
if let Err(e) = store.insert(&synthetic_id, new_count.to_string().as_bytes()) {
110+
log::error!("Error updating KV store: {:?}", e);
111+
}
112+
}
113+
}
114+
115+
// Modify the ad server URL construction to include DMA code if available
116+
let ad_server_url = if advertising_consent {
117+
let mut url = settings
118+
.ad_server
119+
.sync_url
120+
.replace("{{synthetic_id}}", &synthetic_id);
121+
if let Some(dma) = dma_code {
122+
url = format!("{}&dma={}", url, dma);
123+
}
124+
url
125+
} else {
126+
// Use a different URL or parameter for non-personalized ads
127+
settings
128+
.ad_server
129+
.sync_url
130+
.replace("{{synthetic_id}}", "non-personalized")
131+
};
132+
133+
log::info!("Sending request to backend: {}", ad_server_url);
134+
135+
// Add header logging here
136+
let mut ad_req = Request::get(ad_server_url);
137+
138+
// Add consent information to the ad request
139+
ad_req.set_header(
140+
HEADER_X_CONSENT_ADVERTISING,
141+
if advertising_consent { "true" } else { "false" },
142+
);
143+
144+
log::info!("Request headers to Equativ:");
145+
for (name, value) in ad_req.get_headers() {
146+
log::info!(" {}: {:?}", name, value);
147+
}
148+
149+
match ad_req.send(settings.ad_server.ad_partner_url.as_str()) {
150+
Ok(mut res) => {
151+
log::info!(
152+
"Received response from backend with status: {}",
153+
res.get_status()
154+
);
155+
156+
// Extract Fastly PoP from the Compute environment
157+
let fastly_pop = env::var("FASTLY_POP").unwrap_or_else(|_| "unknown".to_string());
158+
let fastly_cache_generation =
159+
env::var("FASTLY_CACHE_GENERATION").unwrap_or_else(|_| "unknown".to_string());
160+
let fastly_customer_id =
161+
env::var("FASTLY_CUSTOMER_ID").unwrap_or_else(|_| "unknown".to_string());
162+
let fastly_hostname =
163+
env::var("FASTLY_HOSTNAME").unwrap_or_else(|_| "unknown".to_string());
164+
let fastly_region = env::var("FASTLY_REGION").unwrap_or_else(|_| "unknown".to_string());
165+
let fastly_service_id =
166+
env::var("FASTLY_SERVICE_ID").unwrap_or_else(|_| "unknown".to_string());
167+
// let fastly_service_version = env::var("FASTLY_SERVICE_VERSION").unwrap_or_else(|_| "unknown".to_string());
168+
let fastly_trace_id =
169+
env::var("FASTLY_TRACE_ID").unwrap_or_else(|_| "unknown".to_string());
170+
171+
log::info!("Fastly Jason PoP: {}", fastly_pop);
172+
log::info!("Fastly Compute Variables:");
173+
log::info!(" - FASTLY_CACHE_GENERATION: {}", fastly_cache_generation);
174+
log::info!(" - FASTLY_CUSTOMER_ID: {}", fastly_customer_id);
175+
log::info!(" - FASTLY_HOSTNAME: {}", fastly_hostname);
176+
log::info!(" - FASTLY_POP: {}", fastly_pop);
177+
log::info!(" - FASTLY_REGION: {}", fastly_region);
178+
log::info!(" - FASTLY_SERVICE_ID: {}", fastly_service_id);
179+
//log::info!(" - FASTLY_SERVICE_VERSION: {}", fastly_service_version);
180+
log::info!(" - FASTLY_TRACE_ID: {}", fastly_trace_id);
181+
182+
// Log all response headers
183+
log::info!("Response headers from Equativ:");
184+
for (name, value) in res.get_headers() {
185+
log::info!(" {}: {:?}", name, value);
186+
}
187+
188+
if res.get_status().is_success() {
189+
let body = res.take_body_str();
190+
log::info!("Backend response body: {}", body);
191+
192+
// Parse the JSON response and extract opid
193+
if let Ok(ad_response) = serde_json::from_str::<AdResponse>(&body) {
194+
// Look for the callback with type "impression"
195+
if let Some(callback) = ad_response
196+
.callbacks
197+
.iter()
198+
.find(|c| c.callback_type == "impression")
199+
{
200+
// Extract opid from the URL
201+
if let Some(opid) = callback
202+
.url
203+
.split('&')
204+
.find(|&param| param.starts_with("opid="))
205+
.and_then(|param| param.split('=').nth(1))
206+
{
207+
log::info!("Found opid: {}", opid);
208+
209+
// Store in opid KV store
210+
log::info!(
211+
"Attempting to open KV store: {}",
212+
settings.synthetic.opid_store
213+
);
214+
match KVStore::open(settings.synthetic.opid_store.as_str()) {
215+
Ok(Some(store)) => {
216+
log::info!("Successfully opened KV store");
217+
match store.insert(&synthetic_id, opid.as_bytes()) {
218+
Ok(_) => log::info!(
219+
"Successfully stored opid {} for synthetic ID: {}",
220+
opid,
221+
synthetic_id
222+
),
223+
Err(e) => {
224+
log::error!("Error storing opid in KV store: {:?}", e)
225+
}
226+
}
227+
}
228+
Ok(None) => {
229+
log::warn!(
230+
"KV store returned None: {}",
231+
settings.synthetic.opid_store
232+
);
233+
}
234+
Err(e) => {
235+
log::error!(
236+
"Error opening KV store {}: {:?}",
237+
settings.synthetic.opid_store,
238+
e
239+
);
240+
}
241+
}
242+
} else {
243+
log::warn!("Could not extract opid from impression callback URL");
244+
}
245+
} else {
246+
log::warn!("No impression callback found in ad response");
247+
}
248+
} else {
249+
log::warn!("Could not parse JSON response to extract opid");
250+
}
251+
252+
let synthetic_header = req
253+
.get_header(HEADER_SYNTHETIC_TRUSTED_SERVER)
254+
.map(|h| h.to_str().unwrap_or(""));
255+
log::info!(
256+
"Returning response with Synthetic header: {:?}",
257+
synthetic_header
258+
);
259+
log::info!("Advertising consent: {}", advertising_consent);
260+
261+
// Return the response to the client
262+
Ok(Response::from_body(body)
263+
.with_status(res.get_status())
264+
.with_header(header::CONTENT_TYPE, "application/json")
265+
.with_header("X-Synthetic-ID", &synthetic_id)
266+
.with_header(
267+
"X-Consent-Advertising",
268+
if advertising_consent { "true" } else { "false" },
269+
)
270+
.with_header("X-Fastly-PoP", &fastly_pop)
271+
.with_header(HEADER_X_COMPRESS_HINT, "on"))
272+
} else {
273+
Ok(Response::from_status(res.get_status())
274+
.with_header(header::CONTENT_TYPE, "application/json")
275+
.with_header(HEADER_X_COMPRESS_HINT, "on")
276+
.with_body("{}"))
277+
}
278+
}
279+
Err(e) => {
280+
log::error!("Error making backend request: {:?}", e);
281+
Ok(Response::from_status(StatusCode::NO_CONTENT)
282+
.with_header(header::CONTENT_TYPE, "application/json")
283+
.with_header(HEADER_X_COMPRESS_HINT, "on")
284+
.with_body("{}"))
285+
}
286+
}
287+
}

crates/common/src/gdpr.rs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
//! This module provides functionality for managing GDPR consent, including
44
//! consent tracking, data subject requests, and compliance with EU privacy regulations.
55
6+
use error_stack::{Report, ResultExt};
67
use fastly::http::{header, Method, StatusCode};
7-
use fastly::{Error, Request, Response};
8+
use fastly::{Request, Response};
89
use serde::{Deserialize, Serialize};
910
use std::collections::HashMap;
1011

1112
use crate::constants::HEADER_X_SUBJECT_ID;
1213
use crate::cookies;
14+
use crate::error::TrustedServerError;
1315
use crate::settings::Settings;
1416

1517
/// GDPR consent information for a user.
@@ -112,22 +114,41 @@ pub fn create_consent_cookie(settings: &Settings, consent: &GdprConsent) -> Stri
112114
///
113115
/// # Errors
114116
///
115-
/// Returns a Fastly [`Error`] if response creation fails.
116-
pub fn handle_consent_request(settings: &Settings, req: Request) -> Result<Response, Error> {
117+
/// Returns a [`TrustedServerError`] if:
118+
/// - JSON serialization/deserialization fails
119+
/// - Response creation fails
120+
pub fn handle_consent_request(
121+
settings: &Settings,
122+
req: Request,
123+
) -> Result<Response, Report<TrustedServerError>> {
117124
match *req.get_method() {
118125
Method::GET => {
119126
// Return current consent status
120127
let consent = get_consent_from_request(&req).unwrap_or_default();
128+
let json_body = serde_json::to_string(&consent)
129+
.change_context(TrustedServerError::GdprConsent {
130+
message: "Failed to serialize consent data".to_string(),
131+
})?;
132+
121133
Ok(Response::from_status(StatusCode::OK)
122134
.with_header(header::CONTENT_TYPE, "application/json")
123-
.with_body(serde_json::to_string(&consent)?))
135+
.with_body(json_body))
124136
}
125137
Method::POST => {
126138
// Update consent preferences
127-
let consent: GdprConsent = serde_json::from_slice(req.into_body_bytes().as_slice())?;
139+
let consent: GdprConsent = serde_json::from_slice(req.into_body_bytes().as_slice())
140+
.change_context(TrustedServerError::GdprConsent {
141+
message: "Failed to parse consent request body".to_string(),
142+
})?;
143+
144+
let json_body = serde_json::to_string(&consent)
145+
.change_context(TrustedServerError::GdprConsent {
146+
message: "Failed to serialize consent response".to_string(),
147+
})?;
148+
128149
let mut response = Response::from_status(StatusCode::OK)
129150
.with_header(header::CONTENT_TYPE, "application/json")
130-
.with_body(serde_json::to_string(&consent)?);
151+
.with_body(json_body);
131152

132153
response.set_header(
133154
header::SET_COOKIE,
@@ -152,8 +173,13 @@ pub fn handle_consent_request(settings: &Settings, req: Request) -> Result<Respo
152173
///
153174
/// # Errors
154175
///
155-
/// Returns a Fastly [`Error`] if response creation fails.
156-
pub fn handle_data_subject_request(_settings: &Settings, req: Request) -> Result<Response, Error> {
176+
/// Returns a [`TrustedServerError`] if:
177+
/// - Header value extraction fails
178+
/// - JSON serialization fails
179+
pub fn handle_data_subject_request(
180+
_settings: &Settings,
181+
req: Request,
182+
) -> Result<Response, Report<TrustedServerError>> {
157183
match *req.get_method() {
158184
Method::GET => {
159185
// Handle data access request
@@ -163,11 +189,21 @@ pub fn handle_data_subject_request(_settings: &Settings, req: Request) -> Result
163189

164190
// TODO: Implement actual data retrieval from KV store
165191
// For now, return empty user data
166-
data.insert(synthetic_id.to_str()?.to_string(), UserData::default());
192+
let id_str = synthetic_id
193+
.to_str()
194+
.change_context(TrustedServerError::InvalidHeaderValue {
195+
message: "Invalid subject ID header value".to_string(),
196+
})?;
197+
data.insert(id_str.to_string(), UserData::default());
198+
199+
let json_body = serde_json::to_string(&data)
200+
.change_context(TrustedServerError::GdprConsent {
201+
message: "Failed to serialize user data".to_string(),
202+
})?;
167203

168204
Ok(Response::from_status(StatusCode::OK)
169205
.with_header(header::CONTENT_TYPE, "application/json")
170-
.with_body(serde_json::to_string(&data)?))
206+
.with_body(json_body))
171207
} else {
172208
Ok(Response::from_status(StatusCode::BAD_REQUEST).with_body("Missing subject ID"))
173209
}

0 commit comments

Comments
 (0)