Skip to content

Commit 12d7577

Browse files
Add script_handler config to neutralize prebid scripts (#128)
1 parent 3ff308e commit 12d7577

File tree

2 files changed

+167
-9
lines changed

2 files changed

+167
-9
lines changed

crates/common/src/integrations/prebid.rs

Lines changed: 166 additions & 9 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,14 +288,19 @@ impl IntegrationProxy for PrebidIntegration {
265288
let path = req.get_path().to_string();
266289
let method = req.get_method().clone();
267290

268-
if method == Method::GET && path == ROUTE_FIRST_PARTY_AD {
269-
self.handle_first_party_ad(settings, req).await
270-
} else if method == Method::POST && path == ROUTE_THIRD_PARTY_AD {
271-
self.handle_third_party_ad(settings, req).await
272-
} else {
273-
Err(Report::new(Self::error(format!(
291+
match method {
292+
Method::GET if self.config.script_handler.as_ref() == Some(&path) => {
293+
self.handle_script_handler()
294+
}
295+
Method::GET if path == ROUTE_FIRST_PARTY_AD => {
296+
self.handle_first_party_ad(settings, req).await
297+
}
298+
Method::POST if path == ROUTE_THIRD_PARTY_AD => {
299+
self.handle_third_party_ad(settings, req).await
300+
}
301+
_ => Err(Report::new(Self::error(format!(
274302
"Unsupported Prebid route: {path}"
275-
))))
303+
)))),
276304
}
277305
}
278306
}
@@ -691,6 +719,7 @@ mod tests {
691719
bidders: vec!["exampleBidder".to_string()],
692720
auto_configure: true,
693721
debug: false,
722+
script_handler: None,
694723
}
695724
}
696725

@@ -957,4 +986,132 @@ mod tests {
957986
));
958987
assert!(!is_prebid_script_url("https://cdn.com/app.js"));
959988
}
989+
990+
#[test]
991+
fn test_script_handler_config_parsing() {
992+
let toml_str = r#"
993+
[publisher]
994+
domain = "test-publisher.com"
995+
cookie_domain = ".test-publisher.com"
996+
origin_url = "https://origin.test-publisher.com"
997+
proxy_secret = "test-secret"
998+
999+
[synthetic]
1000+
counter_store = "test-counter-store"
1001+
opid_store = "test-opid-store"
1002+
secret_key = "test-secret-key"
1003+
template = "{{client_ip}}:{{user_agent}}"
1004+
1005+
[integrations.prebid]
1006+
enabled = true
1007+
server_url = "https://prebid.example"
1008+
script_handler = "/prebid.js"
1009+
"#;
1010+
1011+
let settings = Settings::from_toml(toml_str).expect("should parse TOML");
1012+
let config = settings
1013+
.integration_config::<PrebidIntegrationConfig>("prebid")
1014+
.expect("should get config")
1015+
.expect("should be enabled");
1016+
1017+
assert_eq!(config.script_handler, Some("/prebid.js".to_string()));
1018+
}
1019+
1020+
#[test]
1021+
fn test_script_handler_none_by_default() {
1022+
let toml_str = r#"
1023+
[publisher]
1024+
domain = "test-publisher.com"
1025+
cookie_domain = ".test-publisher.com"
1026+
origin_url = "https://origin.test-publisher.com"
1027+
proxy_secret = "test-secret"
1028+
1029+
[synthetic]
1030+
counter_store = "test-counter-store"
1031+
opid_store = "test-opid-store"
1032+
secret_key = "test-secret-key"
1033+
template = "{{client_ip}}:{{user_agent}}"
1034+
1035+
[integrations.prebid]
1036+
enabled = true
1037+
server_url = "https://prebid.example"
1038+
"#;
1039+
1040+
let settings = Settings::from_toml(toml_str).expect("should parse TOML");
1041+
let config = settings
1042+
.integration_config::<PrebidIntegrationConfig>("prebid")
1043+
.expect("should get config")
1044+
.expect("should be enabled");
1045+
1046+
assert_eq!(config.script_handler, None);
1047+
}
1048+
1049+
#[test]
1050+
fn test_script_handler_returns_empty_js() {
1051+
let config = PrebidIntegrationConfig {
1052+
enabled: true,
1053+
server_url: "https://prebid.example".to_string(),
1054+
timeout_ms: 1000,
1055+
bidders: vec![],
1056+
auto_configure: false,
1057+
debug: false,
1058+
script_handler: Some("/prebid.js".to_string()),
1059+
};
1060+
let integration = PrebidIntegration::new(config);
1061+
1062+
let response = integration
1063+
.handle_script_handler()
1064+
.expect("should return response");
1065+
1066+
assert_eq!(response.get_status(), StatusCode::OK);
1067+
1068+
let content_type = response
1069+
.get_header_str(header::CONTENT_TYPE)
1070+
.expect("should have content-type");
1071+
assert_eq!(content_type, "application/javascript; charset=utf-8");
1072+
1073+
let cache_control = response
1074+
.get_header_str(header::CACHE_CONTROL)
1075+
.expect("should have cache-control");
1076+
assert!(cache_control.contains("max-age=31536000"));
1077+
assert!(cache_control.contains("immutable"));
1078+
1079+
let body = response.into_body_str();
1080+
assert!(body.contains("// Script overridden by Trusted Server"));
1081+
}
1082+
1083+
#[test]
1084+
fn test_routes_includes_script_handler() {
1085+
let config = PrebidIntegrationConfig {
1086+
enabled: true,
1087+
server_url: "https://prebid.example".to_string(),
1088+
timeout_ms: 1000,
1089+
bidders: vec![],
1090+
auto_configure: false,
1091+
debug: false,
1092+
script_handler: Some("/prebid.js".to_string()),
1093+
};
1094+
let integration = PrebidIntegration::new(config);
1095+
1096+
let routes = integration.routes();
1097+
1098+
// Should have 3 routes: first-party ad, third-party ad, and script handler
1099+
assert_eq!(routes.len(), 3);
1100+
1101+
let has_script_route = routes
1102+
.iter()
1103+
.any(|r| r.path == "/prebid.js" && r.method == Method::GET);
1104+
assert!(has_script_route, "should register script handler route");
1105+
}
1106+
1107+
#[test]
1108+
fn test_routes_without_script_handler() {
1109+
let config = base_config(); // Has script_handler: None
1110+
let integration = PrebidIntegration::new(config);
1111+
1112+
let routes = integration.routes();
1113+
1114+
// Should only have 2 routes: first-party ad and third-party ad
1115+
assert_eq!(routes.len(), 2);
1116+
}
9601117
}

trusted-server.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ timeout_ms = 1000
4040
bidders = ["kargo", "rubicon", "appnexus", "openx"]
4141
auto_configure = false
4242
debug = false
43+
# script_handler = "/prebid.js"
4344

4445
[integrations.nextjs]
4546
enabled = false

0 commit comments

Comments
 (0)