Skip to content
Closed
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/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub mod prebid_proxy;
pub mod proxy;
pub mod publisher;
pub mod request_signing;
pub mod script_overrides;
pub mod settings;
pub mod settings_data;
pub mod streaming_processor;
Expand Down
53 changes: 53 additions & 0 deletions crates/common/src/script_overrides.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use fastly::http::StatusCode;
use fastly::Response;

use crate::settings::Settings;

/// Handles requests for overridden scripts by returning an empty JavaScript response.
///
/// This is useful for blocking or stubbing specific script files without breaking
/// the page. The response includes appropriate headers for caching and content type.
///
/// # Returns
///
/// Returns an HTTP 200 response with:
/// - Empty body with a comment explaining the override
/// - `Content-Type: application/javascript; charset=utf-8`
/// - Long cache headers for optimal CDN performance
#[allow(unused)]
pub fn handle_script_override(_settings: &Settings) -> Response {
let body = "// Script overridden by Trusted Server\n";

Response::from_status(StatusCode::OK)
.with_header("Content-Type", "application/javascript; charset=utf-8")
.with_header("Cache-Control", "public, max-age=31536000, immutable")
.with_body(body)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::tests::create_test_settings;

#[test]
fn test_handle_script_override_returns_empty_js() {
let settings = create_test_settings();
let response = handle_script_override(&settings);

assert_eq!(response.get_status(), StatusCode::OK);

let content_type = response
.get_header_str("Content-Type")
.expect("Content-Type header should be present");
assert_eq!(content_type, "application/javascript; charset=utf-8");

let cache_control = response
.get_header_str("Cache-Control")
.expect("Cache-Control header should be present");
assert!(cache_control.contains("max-age=31536000"));
assert!(cache_control.contains("immutable"));

let body = response.into_body_str();
assert!(body.contains("// Script overridden by Trusted Server"));
}
}
98 changes: 98 additions & 0 deletions crates/common/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ pub struct Rewrite {
pub exclude_domains: Vec<String>,
}

#[derive(Debug, Default, Deserialize, Serialize, Validate)]
pub struct ScriptOverrides {
/// List of URL paths that should return empty JavaScript responses.
/// Supports exact path matching only.
#[serde(default)]
pub paths: Vec<String>,
}

impl Rewrite {
/// Checks if a URL should be excluded from rewriting based on domain matching
#[allow(dead_code)]
Expand Down Expand Up @@ -279,6 +287,14 @@ impl Rewrite {
}
}

impl ScriptOverrides {
/// Checks if a path should be overridden with an empty script
#[allow(dead_code)]
pub fn is_overridden(&self, path: &str) -> bool {
self.paths.iter().any(|p| p == path)
}
}

#[derive(Debug, Default, Deserialize, Serialize, Validate)]
pub struct Handler {
#[validate(length(min = 1), custom(function = validate_path))]
Expand Down Expand Up @@ -336,6 +352,9 @@ pub struct Settings {
#[serde(default)]
#[validate(nested)]
pub rewrite: Rewrite,
#[serde(default)]
#[validate(nested)]
pub script_overrides: ScriptOverrides,
}

#[allow(unused)]
Expand Down Expand Up @@ -957,4 +976,83 @@ mod tests {
assert!(!rewrite.is_excluded("not a url"));
assert!(!rewrite.is_excluded(""));
}

#[test]
fn test_script_overrides_is_overridden() {
let overrides = ScriptOverrides {
paths: vec![
"/.static/prebid/1.0.8/prebid.min.js".to_string(),
"/js/analytics.js".to_string(),
],
};

// Should match exact paths
assert!(overrides.is_overridden("/.static/prebid/1.0.8/prebid.min.js"));
assert!(overrides.is_overridden("/js/analytics.js"));

// Should not match similar but different paths
assert!(!overrides.is_overridden("/.static/prebid/1.0.9/prebid.min.js"));
assert!(!overrides.is_overridden("/js/analytics.min.js"));
assert!(!overrides.is_overridden("/other/path.js"));
assert!(!overrides.is_overridden(""));
}

#[test]
fn test_script_overrides_empty() {
let overrides = ScriptOverrides { paths: vec![] };

assert!(!overrides.is_overridden("/.static/prebid/1.0.8/prebid.min.js"));
assert!(!overrides.is_overridden("/js/analytics.js"));
}

#[test]
fn test_script_overrides_from_toml() {
let toml_str = r#"
[publisher]
domain = "test-publisher.com"
cookie_domain = ".test-publisher.com"
origin_url = "https://origin.test-publisher.com"
proxy_secret = "test-secret"

[prebid]
server_url = "https://test-prebid.com/openrtb2/auction"

[synthetic]
counter_store = "test-counter-store"
opid_store = "test-opid-store"
secret_key = "test-secret-key"
template = "{{client_ip}}:{{user_agent}}"

[script_overrides]
paths = [
"/.static/prebid/1.0.8/prebid.min.js",
"/js/old-analytics.js"
]
"#;

let settings = Settings::from_toml(toml_str).expect("should parse TOML");

assert_eq!(settings.script_overrides.paths.len(), 2);
assert!(settings
.script_overrides
.is_overridden("/.static/prebid/1.0.8/prebid.min.js"));
assert!(settings
.script_overrides
.is_overridden("/js/old-analytics.js"));
assert!(!settings
.script_overrides
.is_overridden("/js/other-script.js"));
}

#[test]
fn test_script_overrides_default_empty() {
let toml_str = crate_test_settings_str();
let settings = Settings::from_toml(&toml_str).expect("should parse TOML");

// When not specified, script_overrides should default to empty
assert_eq!(settings.script_overrides.paths.len(), 0);
assert!(!settings
.script_overrides
.is_overridden("/.static/prebid/1.0.8/prebid.min.js"));
}
}
7 changes: 7 additions & 0 deletions crates/fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use trusted_server_common::publisher::{handle_publisher_request, handle_tsjs_dyn
use trusted_server_common::request_signing::{
handle_deactivate_key, handle_jwks_endpoint, handle_rotate_key, handle_verify_signature,
};
use trusted_server_common::script_overrides::handle_script_override;
use trusted_server_common::settings::Settings;
use trusted_server_common::settings_data::get_settings;

Expand Down Expand Up @@ -56,6 +57,12 @@ async fn route_request(
let path = req.get_path().to_string();
let method = req.get_method().clone();

// Check if this path should return an empty script override
if settings.script_overrides.is_overridden(&path) {
log::info!("Returning script override for path: {}", path);
return Ok(handle_script_override(&settings));
}

// Match known routes and handle them
let result = match (method, path.as_str()) {
// Serve the tsjs library
Expand Down
8 changes: 8 additions & 0 deletions trusted-server.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,11 @@ rewrite_scripts = true
# exclude_domains = [
# "*.edgecompute.app",
# ]

# Script Override Configuration
# Define paths that should return empty JavaScript responses
# Useful for blocking specific script versions or third-party scripts
[script_overrides]
paths = [
"/.static/prebid/1.0.8/prebid.min.js",
]