Skip to content

Commit ae39ce8

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

File tree

2 files changed

+164
-2
lines changed

2 files changed

+164
-2
lines changed

crates/common/src/integrations/prebid.rs

Lines changed: 163 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,19 @@ impl PrebidIntegration {
159161
handle_prebid_auction(settings, req, &self.config).await
160162
}
161163

164+
fn handle_script_handler(&self) -> Result<Response, Report<TrustedServerError>> {
165+
log::info!("Returning empty script for Prebid script handler");
166+
let body = "// Script overridden by Trusted Server\n";
167+
168+
Ok(Response::from_status(StatusCode::OK)
169+
.with_header(
170+
header::CONTENT_TYPE,
171+
"application/javascript; charset=utf-8",
172+
)
173+
.with_header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
174+
.with_body(body))
175+
}
176+
162177
async fn handle_first_party_ad(
163178
&self,
164179
settings: &Settings,
@@ -251,10 +266,19 @@ pub fn register(settings: &Settings) -> Option<IntegrationRegistration> {
251266
#[async_trait(?Send)]
252267
impl IntegrationProxy for PrebidIntegration {
253268
fn routes(&self) -> Vec<IntegrationEndpoint> {
254-
vec![
269+
let mut routes = vec![
255270
IntegrationEndpoint::get(ROUTE_FIRST_PARTY_AD),
256271
IntegrationEndpoint::post(ROUTE_THIRD_PARTY_AD),
257-
]
272+
];
273+
274+
if let Some(script_path) = &self.config.script_handler {
275+
// We need to leak the string to get a 'static str for IntegrationEndpoint
276+
// This is safe because the config lives for the lifetime of the application
277+
let static_path: &'static str = Box::leak(script_path.clone().into_boxed_str());
278+
routes.push(IntegrationEndpoint::get(static_path));
279+
}
280+
281+
routes
258282
}
259283

260284
async fn handle(
@@ -265,6 +289,14 @@ impl IntegrationProxy for PrebidIntegration {
265289
let path = req.get_path().to_string();
266290
let method = req.get_method().clone();
267291

292+
if method == Method::GET {
293+
if let Some(script_path) = &self.config.script_handler {
294+
if path == *script_path {
295+
return self.handle_script_handler();
296+
}
297+
}
298+
}
299+
268300
if method == Method::GET && path == ROUTE_FIRST_PARTY_AD {
269301
self.handle_first_party_ad(settings, req).await
270302
} else if method == Method::POST && path == ROUTE_THIRD_PARTY_AD {
@@ -688,6 +720,7 @@ mod tests {
688720
bidders: vec!["exampleBidder".to_string()],
689721
auto_configure: true,
690722
debug: false,
723+
script_handler: None,
691724
}
692725
}
693726

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

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)