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
92 changes: 91 additions & 1 deletion crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use error_stack::Report;
use fastly::http::Method;
use fastly::http::{header, Method};
use fastly::{Error, Request, Response};
use log_fastly::Logger;

Expand Down Expand Up @@ -87,6 +87,33 @@ fn main(req: Request) -> Result<Response, Error> {
))
}

fn build_ja4_debug_response(req: &Request) -> Response {
let ja4 = req.get_tls_ja4().unwrap_or("unavailable");
let h2 = req.get_client_h2_fingerprint().unwrap_or("unavailable");
let cipher = req.get_tls_cipher_openssl_name().unwrap_or("unavailable");
let tls_version = req.get_tls_protocol().unwrap_or("unavailable");
let ua = req.get_header_str("user-agent").unwrap_or("none");
let ch_mobile = req.get_header_str("sec-ch-ua-mobile").unwrap_or("not sent");
let ch_platform = req
.get_header_str("sec-ch-ua-platform")
.unwrap_or("not sent");

let body = format!(
"ja4: {ja4}\n\
h2_fp: {h2}\n\
cipher: {cipher}\n\
tls_version: {tls_version}\n\
user-agent: {ua}\n\
ch-mobile: {ch_mobile}\n\
ch-platform: {ch_platform}\n"
);

Response::from_status(fastly::http::StatusCode::OK)
.with_header(header::CACHE_CONTROL, "no-store, private")
.with_content_type(fastly::mime::TEXT_PLAIN_UTF_8)
.with_body(body)
}

async fn route_request(
settings: &Settings,
orchestrator: &AuctionOrchestrator,
Expand Down Expand Up @@ -160,6 +187,9 @@ async fn route_request(
}
}

// Temporary JA4/TLS debug endpoint for browser fingerprint inspection.
(Method::GET, "/_ts/debug/ja4") => Ok(build_ja4_debug_response(&req)),

// tsjs endpoints
(Method::GET, "/first-party/proxy") => {
handle_first_party_proxy(settings, runtime_services, req).await
Expand Down Expand Up @@ -288,3 +318,63 @@ fn init_logger() {
.apply()
.expect("should initialize logger");
}

#[cfg(test)]
mod tests {
use super::*;
use fastly::mime;

#[test]
fn ja4_debug_response_uses_plain_text_and_fallback_values() {
let req = Request::get("https://example.com/_ts/debug/ja4");

let mut response = build_ja4_debug_response(&req);

assert_eq!(
response.get_status(),
fastly::http::StatusCode::OK,
"should return 200 OK"
);
assert_eq!(
response.get_content_type(),
Some(mime::TEXT_PLAIN_UTF_8),
"should return plain text content"
);
assert_eq!(
response.get_header_str("cache-control"),
Some("no-store, private"),
"should disable caching for the debug response"
);

let body = response.take_body_str();

assert!(
body.contains("ja4: unavailable"),
"should include JA4 fallback"
);
assert!(
body.contains("h2_fp: unavailable"),
"should include H2 fingerprint fallback"
);
assert!(
body.contains("cipher: unavailable"),
"should include cipher fallback"
);
assert!(
body.contains("tls_version: unavailable"),
"should include TLS version fallback"
);
assert!(
body.contains("user-agent: none"),
"should include user-agent fallback"
);
assert!(
body.contains("ch-mobile: not sent"),
"should include sec-ch-ua-mobile fallback"
);
assert!(
body.contains("ch-platform: not sent"),
"should include sec-ch-ua-platform fallback"
);
}
}
68 changes: 67 additions & 1 deletion crates/trusted-server-adapter-fastly/src/route_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::Arc;
use edgezero_core::key_value_store::NoopKvStore;
use error_stack::Report;
use fastly::http::StatusCode;
use fastly::Request;
use fastly::{mime, Request};
use trusted_server_core::auction::build_orchestrator;
use trusted_server_core::integrations::IntegrationRegistry;
use trusted_server_core::platform::{
Expand Down Expand Up @@ -249,3 +249,69 @@ fn configured_missing_consent_store_only_breaks_consent_routes() {
"should scope consent store failures to the consent-dependent routes"
);
}

#[test]
fn ja4_debug_route_returns_plain_text_fallback_response() {
let settings = create_test_settings();
let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator");
let integration_registry =
IntegrationRegistry::new(&settings).expect("should create integration registry");

let req = Request::get("https://test.com/_ts/debug/ja4");
let runtime_services = test_runtime_services(&req);
let mut response = futures::executor::block_on(route_request(
&settings,
&orchestrator,
&integration_registry,
&runtime_services,
req,
))
.expect("should route ja4 debug request");

assert_eq!(
response.get_status(),
StatusCode::OK,
"should return 200 OK for the ja4 debug route"
);
assert_eq!(
response.get_content_type(),
Some(mime::TEXT_PLAIN_UTF_8),
"should return plain text content for the ja4 debug route"
);
assert_eq!(
response.get_header_str("cache-control"),
Some("no-store, private"),
"should disable caching for the ja4 debug route"
);

let body = response.take_body_str();

assert!(
body.contains("ja4: unavailable"),
"should include the JA4 fallback when Fastly omits the fingerprint"
);
assert!(
body.contains("h2_fp: unavailable"),
"should include the H2 fingerprint fallback when Fastly omits it"
);
assert!(
body.contains("cipher: unavailable"),
"should include the cipher fallback when Fastly omits it"
);
assert!(
body.contains("tls_version: unavailable"),
"should include the TLS version fallback when Fastly omits it"
);
assert!(
body.contains("user-agent: none"),
"should include the user-agent fallback when the header is absent"
);
assert!(
body.contains("ch-mobile: not sent"),
"should include the mobile client hints fallback when the header is absent"
);
assert!(
body.contains("ch-platform: not sent"),
"should include the platform client hints fallback when the header is absent"
);
}
Loading