Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 2 additions & 162 deletions crates/trusted-server-core/src/integrations/prebid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ use std::sync::Arc;
use std::time::Duration;

use async_trait::async_trait;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use error_stack::{Report, ResultExt};
use fastly::http::{header, Method, StatusCode, Url};
use fastly::{Request, Response};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as Json};
use serde_json::Value as Json;
use validator::Validate;

use crate::auction::provider::AuctionProvider;
Expand Down Expand Up @@ -417,88 +416,6 @@ fn expand_trusted_server_bidders(
})
.collect()
}
fn transform_prebid_response(
response: &mut Json,
request_host: &str,
request_scheme: &str,
) -> Result<(), Report<TrustedServerError>> {
if let Some(seatbids) = response["seatbid"].as_array_mut() {
for seatbid in seatbids {
if let Some(bids) = seatbid["bid"].as_array_mut() {
for bid in bids {
if let Some(adm) = bid["adm"].as_str() {
bid["adm"] = json!(rewrite_ad_markup(adm, request_host, request_scheme));
}

if let Some(nurl) = bid["nurl"].as_str() {
bid["nurl"] = json!(make_first_party_proxy_url(
nurl,
request_host,
request_scheme,
"track"
));
}

if let Some(burl) = bid["burl"].as_str() {
bid["burl"] = json!(make_first_party_proxy_url(
burl,
request_host,
request_scheme,
"track"
));
}
}
}
}
}

Ok(())
}

fn rewrite_ad_markup(markup: &str, request_host: &str, request_scheme: &str) -> String {
let mut content = markup.to_string();
let cdn_patterns = [
("https://cdn.adsrvr.org", "adsrvr"),
("https://ib.adnxs.com", "adnxs"),
("https://rtb.openx.net", "openx"),
("https://as.casalemedia.com", "casale"),
("https://eus.rubiconproject.com", "rubicon"),
];

for (cdn_url, cdn_name) in cdn_patterns {
if content.contains(cdn_url) {
let proxy_base = format!(
"{}://{}/ad-proxy/{}",
request_scheme, request_host, cdn_name
);
content = content.replace(cdn_url, &proxy_base);
}
}

content = content.replace(
"//cdn.adsrvr.org",
&format!("//{}/ad-proxy/adsrvr", request_host),
);
content = content.replace(
"//ib.adnxs.com",
&format!("//{}/ad-proxy/adnxs", request_host),
);
content
}

fn make_first_party_proxy_url(
third_party_url: &str,
request_host: &str,
request_scheme: &str,
proxy_type: &str,
) -> String {
let encoded = BASE64.encode(third_party_url.as_bytes());
format!(
"{}://{}/ad-proxy/{}/{}",
request_scheme, request_host, proxy_type, encoded
)
}

/// Copies browser headers to the outgoing Prebid Server request.
///
/// In [`ConsentForwardingMode::OpenrtbOnly`] mode, consent cookies are
Expand Down Expand Up @@ -1189,7 +1106,7 @@ impl AuctionProvider for PrebidAuctionProvider {
return Ok(AuctionResponse::error("prebid", response_time_ms));
}

let mut response_json: Json =
let response_json: Json =
serde_json::from_slice(&body_bytes).change_context(TrustedServerError::Prebid {
message: "Failed to parse Prebid response".to_string(),
})?;
Expand All @@ -1205,27 +1122,6 @@ impl AuctionProvider for PrebidAuctionProvider {
}
}

let request_host = response_json
.get("ext")
.and_then(|ext| ext.get("trusted_server"))
.and_then(|trusted_server| trusted_server.get("request_host"))
.and_then(|value| value.as_str())
.unwrap_or("")
.to_string();
let request_scheme = response_json
.get("ext")
.and_then(|ext| ext.get("trusted_server"))
.and_then(|trusted_server| trusted_server.get("request_scheme"))
.and_then(|value| value.as_str())
.unwrap_or("https")
.to_string();

if request_host.is_empty() {
log::warn!("Prebid response missing request host; skipping URL rewrites");
} else {
transform_prebid_response(&mut response_json, &request_host, &request_scheme)?;
}

let mut auction_response = self.parse_openrtb_response(&response_json, response_time_ms);
self.enrich_response_metadata(&response_json, &mut auction_response);

Expand Down Expand Up @@ -1570,62 +1466,6 @@ template = "{{client_ip}}:{{user_agent}}"
);
}

#[test]
fn transform_prebid_response_rewrites_creatives_and_tracking() {
let mut response = json!({
"seatbid": [{
"bid": [{
"adm": r#"<img src="https://cdn.adsrvr.org/pixel.png">"#,
"nurl": "https://notify.example/win",
"burl": "https://notify.example/bill"
}]
}]
});

transform_prebid_response(&mut response, "pub.example", "https")
.expect("should rewrite response");

let rewritten_adm = response["seatbid"][0]["bid"][0]["adm"]
.as_str()
.expect("adm should be string");
assert!(
rewritten_adm.contains("/ad-proxy/adsrvr"),
"creative markup should proxy CDN urls"
);

for url_field in ["nurl", "burl"] {
let value = response["seatbid"][0]["bid"][0][url_field]
.as_str()
.expect("should get tracking URL");
assert!(
value.contains("/ad-proxy/track/"),
"tracking URLs should be proxied"
);
}
}

#[test]
fn make_first_party_proxy_url_base64_encodes_target() {
let url = "https://cdn.example/path?x=1";
let rewritten = make_first_party_proxy_url(url, "pub.example", "https", "track");
assert!(
rewritten.starts_with("https://pub.example/ad-proxy/track/"),
"proxy prefix should be applied"
);

let encoded = rewritten
.split("/ad-proxy/track/")
.nth(1)
.expect("should have encoded payload after proxy prefix");
let decoded = BASE64
.decode(encoded.as_bytes())
.expect("should decode base64 proxy payload");
assert_eq!(
String::from_utf8(decoded).expect("should be valid UTF-8"),
url
);
}

#[test]
fn matches_script_url_matches_common_variants() {
let integration = PrebidIntegration::new(base_config());
Expand Down
Loading