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
4 changes: 2 additions & 2 deletions crates/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ Behavior is covered by an extensive test suite in `crates/common/src/creative.rs

- `synthetic.rs` generates a deterministic synthetic identifier per user request and exposes helpers:
- `generate_synthetic_id` — creates a fresh HMAC-based ID using request signals.
- `get_synthetic_id` — extracts an existing ID from the `X-Synthetic-Trusted-Server` header or `synthetic_id` cookie.
- `get_synthetic_id` — extracts an existing ID from the `x-psid-ts` header or `synthetic_id` cookie.
- `get_or_generate_synthetic_id` — reuses the existing ID when present, otherwise creates one.
- `publisher.rs::handle_publisher_request` stamps proxied origin responses with `X-Synthetic-Fresh`, `X-Synthetic-Trusted-Server`, and (when absent) issues the `synthetic_id` cookie so the browser keeps the identifier on subsequent requests.
- `publisher.rs::handle_publisher_request` stamps proxied origin responses with `X-Synthetic-Fresh`, `x-psid-ts`, and (when absent) issues the `synthetic_id` cookie so the browser keeps the identifier on subsequent requests.
- `proxy.rs::handle_first_party_proxy` replays the identifier to third-party creative origins by appending `synthetic_id=<value>` to the reconstructed target URL, follows redirects (301/302/303/307/308) up to four hops, and keeps downstream fetches linked to the same user scope.
- `proxy.rs::handle_first_party_click` adds `synthetic_id=<value>` to outbound click redirect URLs so analytics endpoints can associate clicks with impressions without third-party cookies.
32 changes: 32 additions & 0 deletions crates/common/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const TRUSTED_SERVER_OUTPUT_CONFIG_PATH: &str = "../../target/trusted-server-out

fn main() {
merge_toml();
generate_header_constants();
rerun_if_changed();
}

Expand Down Expand Up @@ -57,6 +58,37 @@ fn merge_toml() {
fs::write(dest_path, merged_toml).unwrap_or_else(|_| panic!("Failed to write {:?}", dest_path));
}

fn generate_header_constants() {
// Read and parse the config
let init_config_path = Path::new(TRUSTED_SERVER_INIT_CONFIG_PATH);
let toml_content = fs::read_to_string(init_config_path)
.unwrap_or_else(|_| panic!("Failed to read {:?}", init_config_path));

let settings = settings::Settings::from_toml(&toml_content)
.expect("Failed to parse settings at build time");

// Generate the header name based on psid
let psid = &settings.publisher.psid;
let header_name = format!("x-{}-ts", psid);

// Generate Rust code for the constant
let generated_code = format!(
r#"// This file is auto-generated by build.rs - DO NOT EDIT

pub const HEADER_SYNTHETIC_TRUSTED_SERVER: HeaderName = HeaderName::from_static("{}");
"#,
header_name
);

// Write to OUT_DIR
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let dest_path = Path::new(&out_dir).join("generated_header_constants.rs");
fs::write(&dest_path, generated_code)
.unwrap_or_else(|_| panic!("Failed to write {:?}", dest_path));

println!("cargo:warning=Generated header constant: {}", header_name);
}

fn collect_env_vars(value: &Value, env_vars: &mut HashSet<String>, path: Vec<String>) {
if let Value::Object(map) = value {
for (key, val) in map {
Expand Down
5 changes: 3 additions & 2 deletions crates/common/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use http::header::HeaderName;
pub const HEADER_SYNTHETIC_FRESH: HeaderName = HeaderName::from_static("x-synthetic-fresh");
pub const HEADER_SYNTHETIC_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id");
pub const HEADER_X_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id");
pub const HEADER_SYNTHETIC_TRUSTED_SERVER: HeaderName =
HeaderName::from_static("x-synthetic-trusted-server");

// Generated at build time from publisher.id_prefix in trusted-server.toml
include!(concat!(env!("OUT_DIR"), "/generated_header_constants.rs"));
pub const HEADER_X_CONSENT_ADVERTISING: HeaderName =
HeaderName::from_static("x-consent-advertising");
pub const HEADER_X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
Expand Down
13 changes: 8 additions & 5 deletions crates/common/src/gam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::collections::HashMap;
use std::io::Read;
use uuid::Uuid;

use crate::constants::HEADER_SYNTHETIC_TRUSTED_SERVER;
use crate::error::TrustedServerError;
use crate::gdpr::get_consent_from_request;
use crate::settings::Settings;
Expand Down Expand Up @@ -35,7 +36,7 @@ impl GamRequest {

// Get synthetic ID from request headers
let synthetic_id = req
.get_header("X-Synthetic-Trusted-Server")
.get_header(HEADER_SYNTHETIC_TRUSTED_SERVER)
.and_then(|h| h.to_str().ok())
.unwrap_or("unknown")
.to_string();
Expand Down Expand Up @@ -1077,12 +1078,14 @@ mod tests {
use super::*;
use serde_json::json;

use crate::test_support::tests::create_test_settings;
use crate::{
constants::HEADER_SYNTHETIC_TRUSTED_SERVER, test_support::tests::create_test_settings,
};

fn create_test_request() -> Request {
let mut req = Request::new(Method::GET, "https://example.com/test");
req.set_header(header::USER_AGENT, "Mozilla/5.0 Test Browser");
req.set_header("X-Synthetic-Trusted-Server", "test-synthetic-id-123");
req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, "test-synthetic-id-123");
req
}

Expand Down Expand Up @@ -1171,7 +1174,7 @@ mod tests {
Method::GET,
"https://example.com/test?param=value&special=test%20space",
);
req.set_header("X-Synthetic-Trusted-Server", "test-id");
req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, "test-id");

let gam_req = GamRequest::new(&settings, &req).unwrap();
let url = gam_req.build_golden_url();
Expand Down Expand Up @@ -1215,7 +1218,7 @@ mod tests {
let settings = create_test_settings();
let mut req = Request::new(Method::GET, "https://example.com/gam-test");
req.set_header("X-Consent-Advertising", "true");
req.set_header("X-Synthetic-Trusted-Server", "test-synthetic-id");
req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, "test-synthetic-id");

// Note: This test will fail when actually sending to GAM backend
// In a real test environment, we'd mock the backend response
Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ pub fn handle_main_page(
let fresh_id = generate_synthetic_id(settings, &req)?;

// Check for existing Trusted Server ID in this specific order:
// 1. X-Synthetic-Trusted-Server header
// 1. x-psid-ts header
// 2. Cookie
// 3. Fall back to fresh ID
let synthetic_id = get_or_generate_synthetic_id(settings, &req)?;
Expand Down
14 changes: 14 additions & 0 deletions crates/common/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
/// Secret used to encrypt/decrypt proxied URLs in `/first-party/proxy`.
/// Keep this secret stable to allow existing links to decode.
pub proxy_secret: String,
/// Prefix for the publisher-specific ID header (x-{psid}-ts).
/// Defaults to "psid" if not specified.
#[serde(default = "default_psid")]
pub psid: String,
}

fn default_psid() -> String {
"psid".to_string()
}

impl Publisher {
Expand All @@ -35,7 +43,7 @@
/// ```
/// # use trusted_server_common::settings::Publisher;
/// let publisher = Publisher {
/// domain: "example.com".to_string(),

Check failure on line 46 in crates/common/src/settings.rs

View workflow job for this annotation

GitHub Actions / cargo test

missing field `psid` in initializer of `Publisher`
/// cookie_domain: ".example.com".to_string(),
/// origin_url: "https://origin.example.com:8080".to_string(),
/// proxy_secret: "proxy-secret".to_string(),
Expand Down Expand Up @@ -530,6 +538,7 @@
cookie_domain: ".example.com".to_string(),
origin_url: "https://origin.example.com:8080".to_string(),
proxy_secret: "test-secret".to_string(),
psid: "psid".to_string(),
};
assert_eq!(publisher.origin_host(), "origin.example.com:8080");

Expand All @@ -539,6 +548,7 @@
cookie_domain: ".example.com".to_string(),
origin_url: "https://origin.example.com".to_string(),
proxy_secret: "test-secret".to_string(),
psid: "psid".to_string(),
};
assert_eq!(publisher.origin_host(), "origin.example.com");

Expand All @@ -548,6 +558,7 @@
cookie_domain: ".example.com".to_string(),
origin_url: "http://localhost:9090".to_string(),
proxy_secret: "test-secret".to_string(),
psid: "psid".to_string(),
};
assert_eq!(publisher.origin_host(), "localhost:9090");

Expand All @@ -557,6 +568,7 @@
cookie_domain: ".example.com".to_string(),
origin_url: "localhost:9090".to_string(),
proxy_secret: "test-secret".to_string(),
psid: "psid".to_string(),
};
assert_eq!(publisher.origin_host(), "localhost:9090");

Expand All @@ -566,6 +578,7 @@
cookie_domain: ".example.com".to_string(),
origin_url: "http://192.168.1.1:8080".to_string(),
proxy_secret: "test-secret".to_string(),
psid: "psid".to_string(),
};
assert_eq!(publisher.origin_host(), "192.168.1.1:8080");

Expand All @@ -575,6 +588,7 @@
cookie_domain: ".example.com".to_string(),
origin_url: "http://[::1]:8080".to_string(),
proxy_secret: "test-secret".to_string(),
psid: "psid".to_string(),
};
assert_eq!(publisher.origin_host(), "[::1]:8080");
}
Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/synthetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ pub fn generate_synthetic_id(
/// Gets or creates a synthetic ID from the request.
///
/// Attempts to retrieve an existing synthetic ID from:
/// 1. The `X-Synthetic-Trusted-Server` header
/// 1. The `x-psid-ts` header
/// 2. The `synthetic_id` cookie
///
/// If neither exists, generates a new synthetic ID.
Expand Down
6 changes: 3 additions & 3 deletions crates/common/src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,7 @@ pub const GAM_TEST_TEMPLATE: &str = r#"
try {
const response = await fetch('/');
const freshId = response.headers.get('X-Synthetic-Fresh');
const trustedServerId = response.headers.get('X-Synthetic-Trusted-Server');
const trustedServerId = response.headers.get('x-psid-ts');

const statusDiv = document.getElementById('syntheticStatus');
statusDiv.className = 'status success';
Expand Down Expand Up @@ -1280,14 +1280,14 @@ pub const GAM_TEST_TEMPLATE: &str = r#"
// First get the main page to ensure we have synthetic IDs
const mainResponse = await fetch('/');
const freshId = mainResponse.headers.get('X-Synthetic-Fresh');
const trustedServerId = mainResponse.headers.get('X-Synthetic-Trusted-Server');
const trustedServerId = mainResponse.headers.get('x-psid-ts');

// Now test the GAM request
const response = await fetch('/gam-test', {
headers: {
'X-Consent-Advertising': 'true',
'X-Synthetic-Fresh': freshId || '',
'X-Synthetic-Trusted-Server': trustedServerId || ''
'x-psid-ts': trustedServerId || ''
}
});

Expand Down
2 changes: 1 addition & 1 deletion crates/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ The Rust services (`trusted-server-common`) expose several proxy entry points th

- Endpoint: `handle_publisher_request` (`crates/common/src/publisher.rs`).
- Retrieves or generates the trusted synthetic identifier before Fastly consumes the request body.
- Always stamps the proxied response with `X-Synthetic-Fresh` and `X-Synthetic-Trusted-Server` headers and, when the browser does not already present one, sets the `synthetic_id=<value>` cookie (Secure + SameSite=Lax) bound to the configured publisher domain.
- Always stamps the proxied response with `X-Synthetic-Fresh` and `x-psid-ts` headers and, when the browser does not already present one, sets the `synthetic_id=<value>` cookie (Secure + SameSite=Lax) bound to the configured publisher domain.
- Result: downstream assets fetched through the same first-party origin automatically include the synthetic ID header/cookie so subsequent proxy layers can read it.

### Creative Asset Proxy
Expand Down
1 change: 1 addition & 0 deletions trusted-server.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ domain = "test-publisher.com"
cookie_domain = ".test-publisher.com"
origin_url = "https://origin.test-publisher.com"
proxy_secret = "change-me-proxy-secret"
psid = "psid"

[prebid]
server_url = "http://68.183.113.79:8000"
Expand Down
Loading