From e40a490066acd8ca802cdf04b9b50b1de689b384 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 10 Nov 2026 20:16:50 -0600 Subject: [PATCH 1/2] Retain provider-local HTTP opt-in in the demo baseline --- .../tests/routes.rs | 1 + .../src/app.rs | 3 + .../trusted-server-adapter-fastly/src/app.rs | 1 + crates/trusted-server-adapter-spin/src/app.rs | 2 + .../src/auction/formats.rs | 1 + crates/trusted-server-core/src/auction/mod.rs | 1 + .../src/auction/openrtb.rs | 1 + .../src/auction/openrtb/tests.rs | 1 + .../src/auction/orchestrator.rs | 3 + .../trusted-server-core/src/auction/plan.rs | 82 ++++++++++++++++++- .../src/auction/routing.rs | 1 + .../src/auction_config_types.rs | 1 + crates/trusted-server-core/src/config.rs | 1 + .../src/creative_opportunities.rs | 1 + .../src/integrations/aps.rs | 2 + .../src/integrations/prebid.rs | 3 + docs/guide/auction-orchestration.md | 1 + 17 files changed, 104 insertions(+), 2 deletions(-) diff --git a/crates/trusted-server-adapter-axum/tests/routes.rs b/crates/trusted-server-adapter-axum/tests/routes.rs index 60e8690e01..8c8c066096 100644 --- a/crates/trusted-server-adapter-axum/tests/routes.rs +++ b/crates/trusted-server-adapter-axum/tests/routes.rs @@ -104,6 +104,7 @@ async fn aps_profile_serves_renderer_through_adapter_fallback() { settings.auction.providers.insert( "aps-main".parse().expect("should parse APS provider ID"), trusted_server_core::auction::ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "aps".to_string(), endpoint: "https://aps.example/e/pb/bid".to_string(), diff --git a/crates/trusted-server-adapter-cloudflare/src/app.rs b/crates/trusted-server-adapter-cloudflare/src/app.rs index 851044e0b4..78349f8538 100644 --- a/crates/trusted-server-adapter-cloudflare/src/app.rs +++ b/crates/trusted-server-adapter-cloudflare/src/app.rs @@ -817,6 +817,7 @@ mod tests { settings.auction.providers.insert( "aps-main".parse().expect("should parse APS provider ID"), trusted_server_core::auction::ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "aps".to_string(), endpoint: "https://aps.example/e/pb/bid".to_string(), @@ -883,6 +884,7 @@ mod tests { ( id.parse().expect("should parse provider ID"), trusted_server_core::auction::ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "standard".to_string(), endpoint: format!("https://{id}.example/openrtb"), @@ -927,6 +929,7 @@ mod tests { ( id.parse().expect("should parse provider ID"), trusted_server_core::auction::ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "standard".to_string(), endpoint: format!("https://{id}.example/openrtb"), diff --git a/crates/trusted-server-adapter-fastly/src/app.rs b/crates/trusted-server-adapter-fastly/src/app.rs index 23dd7a43b5..9d5eac2a6f 100644 --- a/crates/trusted-server-adapter-fastly/src/app.rs +++ b/crates/trusted-server-adapter-fastly/src/app.rs @@ -1717,6 +1717,7 @@ mod tests { settings.auction.providers.insert( "aps-main".parse().expect("should parse APS provider ID"), trusted_server_core::auction::ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "aps".to_string(), endpoint: "https://aps.example/e/pb/bid".to_string(), diff --git a/crates/trusted-server-adapter-spin/src/app.rs b/crates/trusted-server-adapter-spin/src/app.rs index ad0aa20cc4..64052a7119 100644 --- a/crates/trusted-server-adapter-spin/src/app.rs +++ b/crates/trusted-server-adapter-spin/src/app.rs @@ -885,6 +885,7 @@ mod tests { ( id.parse().expect("should parse provider ID"), trusted_server_core::auction::ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "standard".to_string(), endpoint: format!("https://{id}.example/openrtb"), @@ -909,6 +910,7 @@ mod tests { settings.auction.providers.insert( "aps-main".parse().expect("should parse APS provider ID"), trusted_server_core::auction::ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "aps".to_string(), endpoint: "https://aps.example/e/pb/bid".to_string(), diff --git a/crates/trusted-server-core/src/auction/formats.rs b/crates/trusted-server-core/src/auction/formats.rs index a0df63d94a..3a38082645 100644 --- a/crates/trusted-server-core/src/auction/formats.rs +++ b/crates/trusted-server-core/src/auction/formats.rs @@ -588,6 +588,7 @@ mod tests { providers: BTreeMap::from([( ProviderId::from_str("pbs-primary").expect("should parse provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "prebid-server".to_string(), endpoint: "https://pbs.example.test/openrtb".to_string(), diff --git a/crates/trusted-server-core/src/auction/mod.rs b/crates/trusted-server-core/src/auction/mod.rs index 4323039284..7855e9d46e 100644 --- a/crates/trusted-server-core/src/auction/mod.rs +++ b/crates/trusted-server-core/src/auction/mod.rs @@ -215,6 +215,7 @@ mod plan_sharing_tests { settings.auction.providers = std::collections::BTreeMap::from([( "aps-main".parse().expect("should parse APS provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "aps".to_string(), endpoint: "https://aps.example/e/pb/bid".to_string(), diff --git a/crates/trusted-server-core/src/auction/openrtb.rs b/crates/trusted-server-core/src/auction/openrtb.rs index d2cae15d02..5285b07f78 100644 --- a/crates/trusted-server-core/src/auction/openrtb.rs +++ b/crates/trusted-server-core/src/auction/openrtb.rs @@ -969,6 +969,7 @@ mod routing_metadata_tests { providers: BTreeMap::from([( provider_id.clone(), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: profile.to_string(), endpoint: if profile == "aps" { diff --git a/crates/trusted-server-core/src/auction/openrtb/tests.rs b/crates/trusted-server-core/src/auction/openrtb/tests.rs index a86da73799..605d56afbe 100644 --- a/crates/trusted-server-core/src/auction/openrtb/tests.rs +++ b/crates/trusted-server-core/src/auction/openrtb/tests.rs @@ -42,6 +42,7 @@ fn config_with_endpoint(profile: &str, profile_config: Value, endpoint: &str) -> providers: BTreeMap::from([( provider_id.clone(), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: profile.to_string(), endpoint: endpoint.to_string(), diff --git a/crates/trusted-server-core/src/auction/orchestrator.rs b/crates/trusted-server-core/src/auction/orchestrator.rs index 204201e605..93902a9239 100644 --- a/crates/trusted-server-core/src/auction/orchestrator.rs +++ b/crates/trusted-server-core/src/auction/orchestrator.rs @@ -2618,6 +2618,7 @@ mod tests { ( ProviderId::from_str(id).expect("should parse fictional provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "standard".to_string(), endpoint: "https://example.test/openrtb".to_string(), @@ -2650,6 +2651,7 @@ mod tests { ( ProviderId::from_str(id).expect("should parse fictional provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "prebid-server".to_string(), endpoint: format!("https://{id}.example.test/openrtb"), @@ -2686,6 +2688,7 @@ mod tests { ( ProviderId::from_str(id).expect("should parse fictional provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "aps".to_string(), endpoint: "https://aps.example/e/pb/bid".to_string(), diff --git a/crates/trusted-server-core/src/auction/plan.rs b/crates/trusted-server-core/src/auction/plan.rs index df36fc09b7..ab80c85168 100644 --- a/crates/trusted-server-core/src/auction/plan.rs +++ b/crates/trusted-server-core/src/auction/plan.rs @@ -135,6 +135,10 @@ pub struct ProviderConfig { pub profile: String, /// Fixed provider endpoint. pub endpoint: String, + /// Permit unencrypted HTTP for this provider only. Defaults to false. + /// Use only for explicitly approved demo/test endpoints. + #[serde(default, skip_serializing_if = "is_false")] + pub allow_insecure_http: bool, /// Optional profile-default timeout override. #[serde(default)] pub timeout_ms: Option, @@ -314,7 +318,12 @@ impl AuctionPlan { "provider `{id}` profile_config must be an object" ))); } - let endpoint = canonicalize_endpoint(&id, registration.id, &raw.endpoint)?; + let endpoint = canonicalize_endpoint( + &id, + registration.id, + &raw.endpoint, + raw.allow_insecure_http, + )?; let timeout_ms = raw .timeout_ms .unwrap_or(match registration.default_timeout { @@ -566,13 +575,14 @@ fn canonicalize_endpoint( provider_id: &ProviderId, profile_id: &str, value: &str, + allow_insecure_http: bool, ) -> Result> { let mut endpoint = Url::parse(value).map_err(|error| { configuration_error(format!( "provider `{provider_id}` endpoint must be an absolute HTTPS URL: {error}" )) })?; - if endpoint.scheme() != "https" + if !(endpoint.scheme() == "https" || (allow_insecure_http && endpoint.scheme() == "http")) || endpoint.host_str().is_none() || !endpoint.username().is_empty() || endpoint.password().is_some() @@ -598,6 +608,10 @@ fn canonicalize_endpoint( Ok(CanonicalProviderEndpoint(endpoint)) } +fn is_false(value: &bool) -> bool { + !*value +} + fn normalize_prebid_server_endpoint(endpoint: &mut Url) { match endpoint.path() { "" | "/" => endpoint.set_path("/openrtb2/auction"), @@ -644,6 +658,7 @@ mod tests { fn provider(profile: &str) -> ProviderConfig { ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: profile.to_string(), endpoint: "https://bid.example/openrtb2/auction".to_string(), @@ -1123,6 +1138,69 @@ mod tests { assert!(AuctionPlan::compile(config(BTreeMap::from([(id("aps"), aps)]))).is_err()); } + #[test] + fn insecure_http_requires_an_explicit_per_provider_opt_in() { + let mut demo = provider("prebid-server"); + demo.endpoint = "http://pbs.example.com:8000".to_string(); + assert!( + AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())]))).is_err() + ); + demo.allow_insecure_http = true; + let plan = AuctionPlan::compile(config(BTreeMap::from([(id("demo"), demo.clone())]))) + .expect("explicit demo HTTP endpoint should compile"); + assert_eq!( + plan.providers()[0].endpoint.as_str(), + "http://pbs.example.com:8000/openrtb2/auction" + ); + let mut other = provider("standard"); + other.endpoint = "http://other.example/bid".to_string(); + assert!( + AuctionPlan::compile(config(BTreeMap::from([ + (id("demo"), demo), + (id("other"), other) + ]))) + .is_err() + ); + } + + #[test] + fn insecure_http_opt_in_does_not_allow_credentials_fragments_or_other_schemes() { + for endpoint in [ + "http://user:pass@pbs.example/bid", + "http://pbs.example/bid#fragment", + "ftp://pbs.example/bid", + "/relative", + ] { + let mut raw = provider("prebid-server"); + raw.allow_insecure_http = true; + raw.endpoint = endpoint.to_string(); + assert!( + AuctionPlan::compile(config(BTreeMap::from([(id("demo"), raw)]))).is_err(), + "should reject {endpoint}" + ); + } + } + + #[test] + fn insecure_http_default_preserves_existing_provider_wire_shape() { + let legacy = serde_json::to_value(provider("prebid-server")).unwrap(); + assert!(legacy.get("allow_insecure_http").is_none()); + assert!( + !serde_json::from_value::(legacy) + .unwrap() + .allow_insecure_http + ); + let mut opted_in = provider("prebid-server"); + opted_in.allow_insecure_http = true; + let encoded = serde_json::to_value(opted_in).unwrap(); + assert_eq!(encoded["allow_insecure_http"], true); + assert!( + serde_json::from_value::(encoded) + .unwrap() + .allow_insecure_http + ); + } + #[test] fn compiler_normalizes_only_prebid_server_origin_and_canonical_paths() { for (configured, expected) in [ diff --git a/crates/trusted-server-core/src/auction/routing.rs b/crates/trusted-server-core/src/auction/routing.rs index 2b8135da3c..f87b5dbd3d 100644 --- a/crates/trusted-server-core/src/auction/routing.rs +++ b/crates/trusted-server-core/src/auction/routing.rs @@ -574,6 +574,7 @@ mod tests { fn provider(profile: &str, routing: RoutingMode) -> ProviderConfig { ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: profile.to_string(), endpoint: format!("https://{profile}.example.test/openrtb"), diff --git a/crates/trusted-server-core/src/auction_config_types.rs b/crates/trusted-server-core/src/auction_config_types.rs index 56171e1ea5..36f7e789d6 100644 --- a/crates/trusted-server-core/src/auction_config_types.rs +++ b/crates/trusted-server-core/src/auction_config_types.rs @@ -169,6 +169,7 @@ impl AuctionConfig { ( id, ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "standard".to_string(), endpoint: format!("https://{name}.example/openrtb2/auction"), diff --git a/crates/trusted-server-core/src/config.rs b/crates/trusted-server-core/src/config.rs index e48f5c2f03..16245bc387 100644 --- a/crates/trusted-server-core/src/config.rs +++ b/crates/trusted-server-core/src/config.rs @@ -647,6 +647,7 @@ formats = [{ width = 300, height = 250 }] settings.auction.providers.insert( "aps-main".parse().expect("should parse APS provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "aps".to_string(), endpoint: "https://aps.example.com/e/pb/bid".to_string(), diff --git a/crates/trusted-server-core/src/creative_opportunities.rs b/crates/trusted-server-core/src/creative_opportunities.rs index 8a20c72965..75c893d4a9 100644 --- a/crates/trusted-server-core/src/creative_opportunities.rs +++ b/crates/trusted-server-core/src/creative_opportunities.rs @@ -2383,6 +2383,7 @@ mod tests { providers: BTreeMap::from([( ProviderId::from_str("pbs-primary").expect("should parse provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "prebid-server".to_string(), endpoint: "https://pbs.example.test/openrtb".to_string(), diff --git a/crates/trusted-server-core/src/integrations/aps.rs b/crates/trusted-server-core/src/integrations/aps.rs index 130be1a39f..a0a3e77dcd 100644 --- a/crates/trusted-server-core/src/integrations/aps.rs +++ b/crates/trusted-server-core/src/integrations/aps.rs @@ -1930,6 +1930,7 @@ pub fn register( providers: BTreeMap::from([( "aps".parse().expect("should parse APS provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "aps".to_string(), endpoint: default_endpoint(), @@ -3103,6 +3104,7 @@ mod tests { providers: BTreeMap::from([( "aps".parse().expect("should parse APS provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "aps".to_string(), endpoint: default_endpoint(), diff --git a/crates/trusted-server-core/src/integrations/prebid.rs b/crates/trusted-server-core/src/integrations/prebid.rs index af8a38ca2c..90693534e4 100644 --- a/crates/trusted-server-core/src/integrations/prebid.rs +++ b/crates/trusted-server-core/src/integrations/prebid.rs @@ -5466,6 +5466,7 @@ external_bundle_sri = "sha384-AAAA" ( ProviderId::from_str("pbs-primary").expect("should parse provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "prebid-server".to_string(), endpoint: "https://primary.example.test/openrtb".to_string(), @@ -5478,6 +5479,7 @@ external_bundle_sri = "sha384-AAAA" ( ProviderId::from_str("pbs-secondary").expect("should parse provider ID"), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "prebid-server".to_string(), endpoint: "https://secondary.example.test/openrtb".to_string(), @@ -8988,6 +8990,7 @@ set = { networkId = 42 } providers: BTreeMap::from([( provider_id.clone(), ProviderConfig { + allow_insecure_http: false, protocol: "openrtb-2.6".to_string(), profile: "prebid-server".to_string(), endpoint: "https://pbs.example/openrtb2/auction".to_string(), diff --git a/docs/guide/auction-orchestration.md b/docs/guide/auction-orchestration.md index edbc384719..cf05ddaf00 100644 --- a/docs/guide/auction-orchestration.md +++ b/docs/guide/auction-orchestration.md @@ -763,6 +763,7 @@ Common provider fields and defaults: | `protocol` | Required | `openrtb-2.6` | | `profile` | `standard` | Typed OpenRTB behavior | | `endpoint` | Required | Fixed absolute HTTPS endpoint | +| `allow_insecure_http` | `false` | Explicit per-provider HTTP opt-in for approved demos/tests; traffic is unencrypted | | `timeout_ms` | Profile default | PBS 1000 ms, APS 800 ms, standard inherits auction timeout | | `routing` | `explicit` | `explicit`, or `all_eligible` for non-PBS profiles | | `profile_config` | `{}` | Profile-owned typed settings | From 4aa2ebd49628a40e86a45ffe72a850d78b192bc7 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 10 Nov 2026 20:50:39 -0600 Subject: [PATCH 2/2] Format the retained HTTP provider documentation --- docs/guide/auction-orchestration.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/guide/auction-orchestration.md b/docs/guide/auction-orchestration.md index cf05ddaf00..69abaa2e53 100644 --- a/docs/guide/auction-orchestration.md +++ b/docs/guide/auction-orchestration.md @@ -758,16 +758,16 @@ registered integration selected by `[auction].mediator`. Common provider fields and defaults: -| Field | Default | Meaning | -| ---------------- | --------------- | -------------------------------------------------------------- | -| `protocol` | Required | `openrtb-2.6` | -| `profile` | `standard` | Typed OpenRTB behavior | -| `endpoint` | Required | Fixed absolute HTTPS endpoint | -| `allow_insecure_http` | `false` | Explicit per-provider HTTP opt-in for approved demos/tests; traffic is unencrypted | -| `timeout_ms` | Profile default | PBS 1000 ms, APS 800 ms, standard inherits auction timeout | -| `routing` | `explicit` | `explicit`, or `all_eligible` for non-PBS profiles | -| `profile_config` | `{}` | Profile-owned typed settings | -| `notifications` | No suppression | Common `nurl`/`burl` suppression by all bids or returned seats | +| Field | Default | Meaning | +| --------------------- | --------------- | ---------------------------------------------------------------------------------- | +| `protocol` | Required | `openrtb-2.6` | +| `profile` | `standard` | Typed OpenRTB behavior | +| `endpoint` | Required | Fixed absolute HTTPS endpoint | +| `allow_insecure_http` | `false` | Explicit per-provider HTTP opt-in for approved demos/tests; traffic is unencrypted | +| `timeout_ms` | Profile default | PBS 1000 ms, APS 800 ms, standard inherits auction timeout | +| `routing` | `explicit` | `explicit`, or `all_eligible` for non-PBS profiles | +| `profile_config` | `{}` | Profile-owned typed settings | +| `notifications` | No suppression | Common `nurl`/`burl` suppression by all bids or returned seats | APS normally uses `all_eligible`, which sends every compatible banner slot but never another provider's bidder parameters. `explicit` providers receive only