Skip to content

Commit 02b85da

Browse files
Add script_handler config to neutralize prebid scripts
1 parent 0f3663d commit 02b85da

File tree

2 files changed

+163
-2
lines changed

2 files changed

+163
-2
lines changed

crates/common/src/integrations/prebid.rs

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ pub struct PrebidIntegrationConfig {
4545
pub auto_configure: bool,
4646
#[serde(default)]
4747
pub debug: bool,
48+
#[serde(default)]
49+
pub script_handler: Option<String>,
4850
}
4951

5052
impl IntegrationConfig for PrebidIntegrationConfig {
@@ -159,6 +161,18 @@ impl PrebidIntegration {
159161
handle_prebid_auction(settings, req, &self.config).await
160162
}
161163

164+
fn handle_script_handler(&self) -> Result<Response, Report<TrustedServerError>> {
165+
let body = "// Script overridden by Trusted Server\n";
166+
167+
Ok(Response::from_status(StatusCode::OK)
168+
.with_header(
169+
header::CONTENT_TYPE,
170+
"application/javascript; charset=utf-8",
171+
)
172+
.with_header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
173+
.with_body(body))
174+
}
175+
162176
async fn handle_first_party_ad(
163177
&self,
164178
settings: &Settings,
@@ -251,10 +265,19 @@ pub fn register(settings: &Settings) -> Option<IntegrationRegistration> {
251265
#[async_trait(?Send)]
252266
impl IntegrationProxy for PrebidIntegration {
253267
fn routes(&self) -> Vec<IntegrationEndpoint> {
254-
vec![
268+
let mut routes = vec![
255269
IntegrationEndpoint::get(ROUTE_FIRST_PARTY_AD),
256270
IntegrationEndpoint::post(ROUTE_THIRD_PARTY_AD),
257-
]
271+
];
272+
273+
if let Some(script_path) = &self.config.script_handler {
274+
// We need to leak the string to get a 'static str for IntegrationEndpoint
275+
// This is safe because the config lives for the lifetime of the application
276+
let static_path: &'static str = Box::leak(script_path.clone().into_boxed_str());
277+
routes.push(IntegrationEndpoint::get(static_path));
278+
}
279+
280+
routes
258281
}
259282

260283
async fn handle(
@@ -265,6 +288,14 @@ impl IntegrationProxy for PrebidIntegration {
265288
let path = req.get_path().to_string();
266289
let method = req.get_method().clone();
267290

291+
if method == Method::GET {
292+
if let Some(script_path) = &self.config.script_handler {
293+
if path == *script_path {
294+
return self.handle_script_handler();
295+
}
296+
}
297+
}
298+
268299
if method == Method::GET && path == ROUTE_FIRST_PARTY_AD {
269300
self.handle_first_party_ad(settings, req).await
270301
} else if method == Method::POST && path == ROUTE_THIRD_PARTY_AD {
@@ -688,6 +719,7 @@ mod tests {
688719
bidders: vec!["exampleBidder".to_string()],
689720
auto_configure: true,
690721
debug: false,
722+
script_handler: None,
691723
}
692724
}
693725

@@ -841,4 +873,132 @@ mod tests {
841873
));
842874
assert!(!is_prebid_script_url("https://cdn.com/app.js"));
843875
}
876+
877+
#[test]
878+
fn test_script_handler_config_parsing() {
879+
let toml_str = r#"
880+
[publisher]
881+
domain = "test-publisher.com"
882+
cookie_domain = ".test-publisher.com"
883+
origin_url = "https://origin.test-publisher.com"
884+
proxy_secret = "test-secret"
885+
886+
[synthetic]
887+
counter_store = "test-counter-store"
888+
opid_store = "test-opid-store"
889+
secret_key = "test-secret-key"
890+
template = "{{client_ip}}:{{user_agent}}"
891+
892+
[integrations.prebid]
893+
enabled = true
894+
server_url = "https://prebid.example"
895+
script_handler = "/prebid.js"
896+
"#;
897+
898+
let settings = Settings::from_toml(toml_str).expect("should parse TOML");
899+
let config = settings
900+
.integration_config::<PrebidIntegrationConfig>("prebid")
901+
.expect("should get config")
902+
.expect("should be enabled");
903+
904+
assert_eq!(config.script_handler, Some("/prebid.js".to_string()));
905+
}
906+
907+
#[test]
908+
fn test_script_handler_none_by_default() {
909+
let toml_str = r#"
910+
[publisher]
911+
domain = "test-publisher.com"
912+
cookie_domain = ".test-publisher.com"
913+
origin_url = "https://origin.test-publisher.com"
914+
proxy_secret = "test-secret"
915+
916+
[synthetic]
917+
counter_store = "test-counter-store"
918+
opid_store = "test-opid-store"
919+
secret_key = "test-secret-key"
920+
template = "{{client_ip}}:{{user_agent}}"
921+
922+
[integrations.prebid]
923+
enabled = true
924+
server_url = "https://prebid.example"
925+
"#;
926+
927+
let settings = Settings::from_toml(toml_str).expect("should parse TOML");
928+
let config = settings
929+
.integration_config::<PrebidIntegrationConfig>("prebid")
930+
.expect("should get config")
931+
.expect("should be enabled");
932+
933+
assert_eq!(config.script_handler, None);
934+
}
935+
936+
#[test]
937+
fn test_script_handler_returns_empty_js() {
938+
let config = PrebidIntegrationConfig {
939+
enabled: true,
940+
server_url: "https://prebid.example".to_string(),
941+
timeout_ms: 1000,
942+
bidders: vec![],
943+
auto_configure: false,
944+
debug: false,
945+
script_handler: Some("/prebid.js".to_string()),
946+
};
947+
let integration = PrebidIntegration::new(config);
948+
949+
let response = integration
950+
.handle_script_handler()
951+
.expect("should return response");
952+
953+
assert_eq!(response.get_status(), StatusCode::OK);
954+
955+
let content_type = response
956+
.get_header_str(header::CONTENT_TYPE)
957+
.expect("should have content-type");
958+
assert_eq!(content_type, "application/javascript; charset=utf-8");
959+
960+
let cache_control = response
961+
.get_header_str(header::CACHE_CONTROL)
962+
.expect("should have cache-control");
963+
assert!(cache_control.contains("max-age=31536000"));
964+
assert!(cache_control.contains("immutable"));
965+
966+
let body = response.into_body_str();
967+
assert!(body.contains("// Script overridden by Trusted Server"));
968+
}
969+
970+
#[test]
971+
fn test_routes_includes_script_handler() {
972+
let config = PrebidIntegrationConfig {
973+
enabled: true,
974+
server_url: "https://prebid.example".to_string(),
975+
timeout_ms: 1000,
976+
bidders: vec![],
977+
auto_configure: false,
978+
debug: false,
979+
script_handler: Some("/prebid.js".to_string()),
980+
};
981+
let integration = PrebidIntegration::new(config);
982+
983+
let routes = integration.routes();
984+
985+
// Should have 3 routes: first-party ad, third-party ad, and script handler
986+
assert_eq!(routes.len(), 3);
987+
988+
let has_script_route = routes
989+
.iter()
990+
.any(|r| r.path == "/prebid.js" && r.method == Method::GET);
991+
assert!(has_script_route, "should register script handler route");
992+
}
993+
994+
#[test]
995+
fn test_routes_without_script_handler() {
996+
let config = base_config(); // Has script_handler: None
997+
let integration = PrebidIntegration::new(config);
998+
999+
let routes = integration.routes();
1000+
1001+
// Should only have 2 routes: first-party ad and third-party ad
1002+
assert_eq!(routes.len(), 2);
1003+
}
8441004
}

trusted-server.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ timeout_ms = 1000
4545
bidders = ["kargo", "rubicon", "appnexus", "openx"]
4646
auto_configure = false
4747
debug = false
48+
# script_handler = "/prebid.js"
4849

4950
[integrations.testlight]
5051
endpoint = "https://testlight.example/openrtb2/auction"

0 commit comments

Comments
 (0)