Skip to content
Draft
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions crates/trusted-server-core/benches/html_processor_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ fn make_config() -> HtmlProcessorConfig {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
}
}

Expand Down
61 changes: 61 additions & 0 deletions crates/trusted-server-core/src/html_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use lol_html::{
text,
};

use crate::integrations::datadome::{DATADOME_INTEGRATION_ID, DataDomeClientTagSuppressed};
use crate::integrations::gpt_diagnostics::GptDiagnosticsRequestDecision;
use crate::integrations::{
AttributeRewriteOutcome, IntegrationAttributeContext, IntegrationDocumentState,
Expand Down Expand Up @@ -175,6 +176,8 @@ pub struct HtmlProcessorConfig {
pub max_buffered_body_bytes: usize,
/// Request-scoped conditional diagnostics delivery decision.
pub gpt_diagnostics: Option<GptDiagnosticsRequestDecision>,
/// Whether to omit Trusted Server's automatic `DataDome` client-side tag.
pub suppress_datadome_client_side_tag: bool,
}

impl HtmlProcessorConfig {
Expand All @@ -196,6 +199,7 @@ impl HtmlProcessorConfig {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: settings.publisher.max_buffered_body_bytes,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
}
}

Expand Down Expand Up @@ -223,6 +227,13 @@ impl HtmlProcessorConfig {
self.gpt_diagnostics = decision;
self
}

/// Attach the request-scoped `DataDome` client-tag suppression decision.
#[must_use]
pub fn with_datadome_client_tag_suppression(mut self, suppress: bool) -> Self {
self.suppress_datadome_client_side_tag = suppress;
self
}
}

/// Create an HTML processor with URL replacement and integration hooks.
Expand All @@ -235,6 +246,9 @@ impl HtmlProcessorConfig {
pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcessor {
let post_processors = config.integrations.html_post_processors();
let document_state = IntegrationDocumentState::default();
if config.suppress_datadome_client_side_tag {
document_state.get_or_insert_with(DATADOME_INTEGRATION_ID, || DataDomeClientTagSuppressed);
}

// Simplified URL patterns structure - stores only core data and generates variants on-demand
struct UrlPatterns {
Expand Down Expand Up @@ -692,6 +706,7 @@ mod tests {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
}
}

Expand Down Expand Up @@ -950,6 +965,46 @@ mod tests {
assert_eq!(config.request_scheme, "https");
}

#[test]
fn suppressed_datadome_tag_is_not_injected_into_processed_html() {
let mut settings = create_test_settings();
settings
.integrations
.insert_config(
"datadome",
&json!({
"enabled": true,
"client_side_key": "test-client-key",
}),
)
.expect("should configure DataDome integration");
let registry = IntegrationRegistry::new(&settings)
.expect("should create integration registry with DataDome");
let config = HtmlProcessorConfig::from_settings(
&settings,
&registry,
"origin.example.com",
"test.example.com",
"https",
)
.with_datadome_client_tag_suppression(true);
let mut processor = create_html_processor(config);

let output = processor
.process_chunk(b"<html><head></head><body>content</body></html>", true)
.expect("should process HTML");
let html = String::from_utf8(output).expect("should produce UTF-8 HTML");

assert!(
!html.contains("window.ddjskey"),
"should omit the DataDome client configuration"
);
assert!(
!html.contains("/integrations/datadome/tags.js"),
"should omit the DataDome client tag URL"
);
}

#[test]
fn test_real_publisher_html() {
// Test with publisher HTML from test_publisher.html
Expand Down Expand Up @@ -1539,6 +1594,7 @@ mod tests {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1613,6 +1669,7 @@ mod tests {
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1649,6 +1706,7 @@ mod tests {
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
// Malformed HTML with two <body> elements (common in CMS template pages)
Expand Down Expand Up @@ -1684,6 +1742,7 @@ mod tests {
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1737,6 +1796,7 @@ mod tests {
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1764,6 +1824,7 @@ mod tests {
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
suppress_datadome_client_side_tag: false,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down
36 changes: 32 additions & 4 deletions crates/trusted-server-core/src/integrations/datadome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ pub use protection_scope::{

use protection_scope::ProtectionScope;

pub(super) const DATADOME_INTEGRATION_ID: &str = "datadome";
pub(crate) const DATADOME_INTEGRATION_ID: &str = "datadome";

/// Request marker indicating that Trusted Server should omit its automatic
/// `DataDome` client-side tag for the current response.
#[derive(Debug, Clone, Copy)]
pub(crate) struct DataDomeClientTagSuppressed;

/// Regex pattern for matching and rewriting `DataDome` URLs in script content.
///
Expand Down Expand Up @@ -765,7 +770,15 @@ impl IntegrationHeadInjector for DataDomeIntegration {
DATADOME_INTEGRATION_ID
}

fn head_inserts(&self, _ctx: &IntegrationHtmlContext<'_>) -> Vec<String> {
fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec<String> {
if ctx
.document_state
.get::<DataDomeClientTagSuppressed>(DATADOME_INTEGRATION_ID)
.is_some()
{
return Vec::new();
}

if !self.config.inject_client_side_tag || self.config.client_side_key.trim().is_empty() {
return Vec::new();
}
Expand Down Expand Up @@ -841,9 +854,10 @@ fn build(
};

log::info!(
"[datadome] Registering integration (sdk_origin: {}, rewrite_sdk: {})",
"[datadome] Registering integration (sdk_origin: {}, rewrite_sdk: {}, enable_protection: {})",
config.sdk_origin,
config.rewrite_sdk
config.rewrite_sdk,
config.enable_protection
);

Ok(Some(DataDomeIntegration::try_new(config)?))
Expand Down Expand Up @@ -1248,6 +1262,20 @@ mod tests {

#[test]
fn head_injector_omits_client_side_tag_when_disabled_or_blank() {
let mut suppressed = test_config();
suppressed.client_side_key = "test-client-key".to_string();
let suppressed_integration = DataDomeIntegration::new(suppressed);
let suppressed_state = crate::integrations::IntegrationDocumentState::default();
suppressed_state
.get_or_insert_with(DATADOME_INTEGRATION_ID, || DataDomeClientTagSuppressed);
let suppressed_ctx = html_context_for_tests(&suppressed_state);
assert!(
suppressed_integration
.head_inserts(&suppressed_ctx)
.is_empty(),
"should omit the tag when the request is IP-excluded"
);

let mut blank_key = test_config();
blank_key.client_side_key = " ".to_string();
let integration = DataDomeIntegration::new(blank_key);
Expand Down
Loading
Loading