-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathformats.rs
More file actions
320 lines (291 loc) · 10.7 KB
/
formats.rs
File metadata and controls
320 lines (291 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
//! Auction request/response format conversions.
//!
//! This module handles:
//! - Parsing incoming tsjs/Prebid.js format requests
//! - Converting internal auction results to `OpenRTB` 2.x responses
use error_stack::{ensure, Report, ResultExt};
use fastly::http::{header, StatusCode};
use fastly::{Request, Response};
use serde::Deserialize;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use uuid::Uuid;
use crate::auction::context::ContextValue;
use crate::consent::ConsentContext;
use crate::constants::{HEADER_X_TS_EC, HEADER_X_TS_EC_FRESH};
use crate::creative;
use crate::edge_cookie::generate_ec_id;
use crate::error::TrustedServerError;
use crate::openrtb::{to_openrtb_i32, OpenRtbBid, OpenRtbResponse, ResponseExt, SeatBid, ToExt};
use crate::platform::{GeoInfo, RuntimeServices};
use crate::settings::Settings;
use super::orchestrator::OrchestrationResult;
use super::types::{
AdFormat, AdSlot, AuctionRequest, DeviceInfo, MediaType, OrchestratorExt, ProviderSummary,
PublisherInfo, SiteInfo, UserInfo,
};
/// Request body format for auction endpoints (tsjs/Prebid.js format).
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdRequest {
pub ad_units: Vec<AdUnit>,
pub config: Option<JsonValue>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdUnit {
pub code: String,
pub media_types: Option<MediaTypes>,
pub bids: Option<Vec<BidConfig>>,
}
/// Bidder configuration from the request.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BidConfig {
pub bidder: String,
#[serde(default)]
pub params: serde_json::Value,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaTypes {
pub banner: Option<BannerUnit>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BannerUnit {
pub sizes: Vec<Vec<u32>>,
}
/// Convert tsjs/Prebid.js request format to internal [`AuctionRequest`].
///
/// The `consent` parameter carries decoded consent signals extracted from the
/// incoming request's cookies and headers. It is populated by the caller
/// (the `/auction` endpoint handler) and forwarded through to the
/// [`OpenRTB`][`crate::openrtb::OpenRtbRequest`] bid request.
///
/// The `ec_id` is generated by the caller before the consent pipeline
/// runs, so that KV Store operations can use it as a key.
///
/// # Errors
///
/// Returns an error if:
/// - Fresh EC ID generation fails
/// - Request contains invalid banner sizes (must be [width, height])
pub fn convert_tsjs_to_auction_request(
body: &AdRequest,
settings: &Settings,
services: &RuntimeServices,
req: &Request,
consent: ConsentContext,
ec_id: &str,
geo: Option<GeoInfo>,
) -> Result<AuctionRequest, Report<TrustedServerError>> {
let ec_id = ec_id.to_owned();
let fresh_id =
generate_ec_id(settings, services).change_context(TrustedServerError::Auction {
message: "Failed to generate fresh EC ID".to_string(),
})?;
// Convert ad units to slots
let mut slots = Vec::new();
for unit in &body.ad_units {
if let Some(media_types) = &unit.media_types {
if let Some(banner) = &media_types.banner {
let mut formats = Vec::new();
for size in &banner.sizes {
ensure!(
size.len() == 2,
TrustedServerError::BadRequest {
message: "Invalid banner size; expected [width, height]".to_string(),
}
);
formats.push(AdFormat {
width: size[0],
height: size[1],
media_type: MediaType::Banner,
});
}
// Extract bidder params from the bids array
let mut bidders = HashMap::new();
if let Some(bids) = &unit.bids {
for bid in bids {
bidders.insert(bid.bidder.clone(), bid.params.clone());
}
}
slots.push(AdSlot {
id: unit.code.clone(),
formats,
floor_price: None,
targeting: HashMap::new(),
bidders,
});
}
}
}
// Build device info with user-agent (always) and geo (if available)
let device = Some(DeviceInfo {
user_agent: req
.get_header_str("user-agent")
.map(std::string::ToString::to_string),
ip: services.client_info.client_ip.map(|ip| ip.to_string()),
geo,
});
// Forward allowed config entries from the JS request into the context map.
// Only keys listed in `auction.allowed_context_keys` are accepted;
// unrecognised keys are silently dropped to prevent injection of
// arbitrary data by a malicious client payload.
let mut context = HashMap::new();
if let Some(ref config) = body.config {
if let Some(obj) = config.as_object() {
for (key, value) in obj {
if settings.auction.allowed_context_keys.contains(key) {
match serde_json::from_value::<ContextValue>(value.clone()) {
Ok(cv) => {
context.insert(key.clone(), cv);
}
Err(_) => {
log::debug!(
"Auction context: dropping key '{}' with unsupported type",
key
);
}
}
} else {
log::debug!("Auction context: dropping disallowed key '{}'", key);
}
}
if !context.is_empty() {
log::debug!(
"Auction request context: {} entries ({})",
context.len(),
context.keys().cloned().collect::<Vec<_>>().join(", ")
);
}
}
}
Ok(AuctionRequest {
id: Uuid::new_v4().to_string(),
slots,
publisher: PublisherInfo {
domain: settings.publisher.domain.clone(),
page_url: Some(format!("https://{}", settings.publisher.domain)),
},
user: UserInfo {
id: ec_id,
fresh_id,
consent: Some(consent),
},
device,
site: Some(SiteInfo {
domain: settings.publisher.domain.clone(),
page: format!("https://{}", settings.publisher.domain),
}),
context,
})
}
/// Convert `OrchestrationResult` to `OpenRTB` response format.
///
/// Returns rewritten creative HTML directly in the `adm` field for inline delivery.
///
/// # Errors
///
/// Returns an error if:
/// - A winning bid is missing a price
/// - The response serialization fails
pub fn convert_to_openrtb_response(
result: &OrchestrationResult,
settings: &Settings,
auction_request: &AuctionRequest,
) -> Result<Response, Report<TrustedServerError>> {
// Build OpenRTB-style seatbid array
let mut seatbids = Vec::with_capacity(result.winning_bids.len());
for (slot_id, bid) in &result.winning_bids {
let price = bid.price.ok_or_else(|| {
Report::new(TrustedServerError::Auction {
message: format!(
"Winning bid for slot '{}' from '{}' has no decoded price",
slot_id, bid.bidder
),
})
})?;
let bid_context = format!(
"auction {} slot {} bidder {}",
auction_request.id, slot_id, bid.bidder
);
let width = to_openrtb_i32(bid.width, "width", &bid_context);
let height = to_openrtb_i32(bid.height, "height", &bid_context);
// Process creative HTML if present - — sanitize dangerous markup first, then rewrite URLs.
let creative_html = if let Some(ref raw_creative) = bid.creative {
let sanitized = creative::sanitize_creative_html(raw_creative);
let rewritten = creative::rewrite_creative_html(settings, &sanitized);
log::debug!(
"Processed creative for auction {} slot {} ({} → {} → {} bytes)",
auction_request.id,
slot_id,
raw_creative.len(),
sanitized.len(),
rewritten.len()
);
rewritten
} else {
// No creative provided (e.g., from mediation layer that returns iframe URLs)
log::warn!(
"No creative HTML for auction {} slot {} - mediation should have provided creative",
auction_request.id,
slot_id
);
String::new()
};
let openrtb_bid = OpenRtbBid {
id: Some(format!("{}-{}", bid.bidder, slot_id)),
impid: Some(slot_id.to_string()),
price: Some(price),
adm: Some(creative_html),
crid: Some(format!("{}-creative", bid.bidder)),
w: width,
h: height,
adomain: bid.adomain.clone().unwrap_or_default(),
..Default::default()
};
seatbids.push(SeatBid {
seat: Some(bid.bidder.clone()),
bid: vec![openrtb_bid],
..Default::default()
});
}
// Determine strategy name for response metadata
let strategy_name = if settings.auction.has_mediator() {
"parallel_mediation"
} else {
"parallel_only"
};
// Build per-provider summaries from the orchestration result
let provider_details: Vec<ProviderSummary> = result
.provider_responses
.iter()
.map(ProviderSummary::from)
.collect();
let response_body = OpenRtbResponse {
id: Some(auction_request.id.to_string()),
seatbid: seatbids,
ext: ResponseExt {
orchestrator: OrchestratorExt {
strategy: strategy_name.to_string(),
providers: result.provider_responses.len(),
total_bids: result.total_bids(),
time_ms: result.total_time_ms,
provider_details,
},
}
.to_ext(),
..Default::default()
};
let body_bytes =
serde_json::to_vec(&response_body).change_context(TrustedServerError::Auction {
message: "Failed to serialize auction response".to_string(),
})?;
Ok(Response::from_status(StatusCode::OK)
.with_header(header::CONTENT_TYPE, "application/json")
.with_header(HEADER_X_TS_EC, &auction_request.user.id)
.with_header(HEADER_X_TS_EC_FRESH, &auction_request.user.fresh_id)
.with_body(body_bytes))
}