Skip to content

Commit 6b32e54

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/docs/npm_and_yarn-8a6d6a6aaf
2 parents 16ff76f + ca60741 commit 6b32e54

41 files changed

Lines changed: 4983 additions & 160 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=false
4242
# TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS=kargo,rubicon,appnexus
4343
# TRUSTED_SERVER__INTEGRATIONS__PREBID__AUTO_CONFIGURE=false
4444
# TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=false
45+
# TRUSTED_SERVER__INTEGRATIONS__PREBID__TEST_MODE=false
4546

4647
# Next.js
4748
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__ENABLED=false

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ src/*.html
2626

2727
# SSL certificates
2828
*.pem
29+
30+
/guest-profiles
31+
/benchmark-results/**

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ brotli = "8.0"
4444
build-print = "1.0.1"
4545
bytes = "1.11"
4646
chacha20poly1305 = "0.10"
47-
chrono = "0.4.42"
47+
chrono = "0.4.44"
4848
config = "0.15.19"
4949
cookie = "0.18.1"
5050
derive_more = { version = "2.0", features = ["display", "error"] }

OPTIMIZATION.md

Lines changed: 534 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
//! Context query-parameter forwarding for auction providers.
2+
//!
3+
//! Provides a config-driven mechanism for ad-server / mediator providers to
4+
//! forward integration-supplied data (e.g. audience segments) as URL query
5+
//! parameters without hard-coding integration-specific knowledge.
6+
7+
use serde::{Deserialize, Serialize};
8+
use std::collections::{BTreeMap, HashMap};
9+
10+
/// A strongly-typed context value forwarded from the JS client payload.
11+
///
12+
/// Replaces raw `serde_json::Value` so that consumers get compile-time
13+
/// exhaustiveness checks. The `#[serde(untagged)]` attribute preserves
14+
/// wire-format compatibility — the JS client sends plain JSON arrays, strings,
15+
/// or numbers which serde maps to the matching variant in declaration order.
16+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17+
#[serde(untagged)]
18+
pub enum ContextValue {
19+
/// A list of string values (e.g. audience segment IDs).
20+
StringList(Vec<String>),
21+
/// A single string value.
22+
Text(String),
23+
/// A numeric value.
24+
Number(f64),
25+
}
26+
27+
/// Mapping from auction-request context keys to query-parameter names.
28+
///
29+
/// Used by ad-server / mediator providers to forward integration-supplied data
30+
/// (e.g. audience segments) as URL query parameters without hard-coding
31+
/// integration-specific knowledge.
32+
///
33+
/// ```toml
34+
/// [integrations.adserver_mock.context_query_params]
35+
/// permutive_segments = "permutive"
36+
/// lockr_ids = "lockr"
37+
/// ```
38+
pub type ContextQueryParams = BTreeMap<String, String>;
39+
40+
/// Build a URL by appending context values as query parameters according to the
41+
/// provided mapping.
42+
///
43+
/// For each entry in `mapping`, if the corresponding key exists in `context`:
44+
/// - **Arrays** are serialised as a comma-separated string.
45+
/// - **Strings / numbers** are serialised as-is.
46+
/// - Other JSON types are skipped.
47+
///
48+
/// The [`url::Url`] crate is used for construction so all values are
49+
/// percent-encoded, preventing query-parameter injection.
50+
///
51+
/// Returns the original `base_url` unchanged when no parameters are appended.
52+
#[must_use]
53+
pub fn build_url_with_context_params(
54+
base_url: &str,
55+
context: &HashMap<String, ContextValue>,
56+
mapping: &ContextQueryParams,
57+
) -> String {
58+
let Ok(mut url) = url::Url::parse(base_url) else {
59+
log::warn!("build_url_with_context_params: failed to parse base URL, returning as-is");
60+
return base_url.to_string();
61+
};
62+
63+
let mut appended = 0usize;
64+
65+
for (context_key, param_name) in mapping {
66+
if let Some(value) = context.get(context_key) {
67+
let serialized = serialize_context_value(value);
68+
if !serialized.is_empty() {
69+
url.query_pairs_mut().append_pair(param_name, &serialized);
70+
appended += 1;
71+
}
72+
}
73+
}
74+
75+
if appended > 0 {
76+
log::info!(
77+
"build_url_with_context_params: appended {} context query params",
78+
appended
79+
);
80+
}
81+
82+
url.to_string()
83+
}
84+
85+
/// Serialise a single [`ContextValue`] into a string suitable for a query
86+
/// parameter value. String lists are joined with commas; strings and numbers
87+
/// are returned directly.
88+
fn serialize_context_value(value: &ContextValue) -> String {
89+
match value {
90+
ContextValue::StringList(items) => items.join(","),
91+
ContextValue::Text(s) => s.clone(),
92+
ContextValue::Number(n) => n.to_string(),
93+
}
94+
}
95+
96+
#[cfg(test)]
97+
mod tests {
98+
use super::*;
99+
100+
#[test]
101+
fn test_build_url_with_context_params_appends_array() {
102+
let context = HashMap::from([(
103+
"permutive_segments".to_string(),
104+
ContextValue::StringList(vec!["10000001".into(), "10000003".into(), "adv".into()]),
105+
)]);
106+
let mapping = BTreeMap::from([("permutive_segments".to_string(), "permutive".to_string())]);
107+
108+
let url = build_url_with_context_params(
109+
"http://localhost:6767/adserver/mediate",
110+
&context,
111+
&mapping,
112+
);
113+
assert_eq!(
114+
url,
115+
"http://localhost:6767/adserver/mediate?permutive=10000001%2C10000003%2Cadv"
116+
);
117+
}
118+
119+
#[test]
120+
fn test_build_url_with_context_params_preserves_existing_query() {
121+
let context = HashMap::from([(
122+
"permutive_segments".to_string(),
123+
ContextValue::StringList(vec!["123".into(), "adv".into()]),
124+
)]);
125+
let mapping = BTreeMap::from([("permutive_segments".to_string(), "permutive".to_string())]);
126+
127+
let url = build_url_with_context_params(
128+
"http://localhost:6767/adserver/mediate?debug=true",
129+
&context,
130+
&mapping,
131+
);
132+
assert_eq!(
133+
url,
134+
"http://localhost:6767/adserver/mediate?debug=true&permutive=123%2Cadv"
135+
);
136+
}
137+
138+
#[test]
139+
fn test_build_url_with_context_params_no_matching_keys() {
140+
let context = HashMap::new();
141+
let mapping = BTreeMap::from([("permutive_segments".to_string(), "permutive".to_string())]);
142+
143+
let url = build_url_with_context_params(
144+
"http://localhost:6767/adserver/mediate",
145+
&context,
146+
&mapping,
147+
);
148+
assert_eq!(url, "http://localhost:6767/adserver/mediate");
149+
}
150+
151+
#[test]
152+
fn test_build_url_with_context_params_empty_array_skipped() {
153+
let context = HashMap::from([(
154+
"permutive_segments".to_string(),
155+
ContextValue::StringList(vec![]),
156+
)]);
157+
let mapping = BTreeMap::from([("permutive_segments".to_string(), "permutive".to_string())]);
158+
159+
let url = build_url_with_context_params(
160+
"http://localhost:6767/adserver/mediate",
161+
&context,
162+
&mapping,
163+
);
164+
assert!(!url.contains("permutive="));
165+
}
166+
167+
#[test]
168+
fn test_build_url_with_context_params_multiple_mappings() {
169+
let context = HashMap::from([
170+
(
171+
"permutive_segments".to_string(),
172+
ContextValue::StringList(vec!["seg1".into()]),
173+
),
174+
(
175+
"lockr_ids".to_string(),
176+
ContextValue::Text("lockr-abc-123".into()),
177+
),
178+
]);
179+
let mapping = BTreeMap::from([
180+
("lockr_ids".to_string(), "lockr".to_string()),
181+
("permutive_segments".to_string(), "permutive".to_string()),
182+
]);
183+
184+
let url = build_url_with_context_params(
185+
"http://localhost:6767/adserver/mediate",
186+
&context,
187+
&mapping,
188+
);
189+
assert!(url.contains("permutive=seg1"));
190+
assert!(url.contains("lockr=lockr-abc-123"));
191+
}
192+
193+
#[test]
194+
fn test_build_url_with_context_params_scalar_number() {
195+
let context = HashMap::from([("count".to_string(), ContextValue::Number(42.0))]);
196+
let mapping = BTreeMap::from([("count".to_string(), "n".to_string())]);
197+
198+
let url = build_url_with_context_params(
199+
"http://localhost:6767/adserver/mediate",
200+
&context,
201+
&mapping,
202+
);
203+
assert_eq!(url, "http://localhost:6767/adserver/mediate?n=42");
204+
}
205+
206+
#[test]
207+
fn test_serialize_context_value_string_list() {
208+
assert_eq!(
209+
serialize_context_value(&ContextValue::StringList(vec![
210+
"a".into(),
211+
"b".into(),
212+
"3".into()
213+
])),
214+
"a,b,3"
215+
);
216+
}
217+
218+
#[test]
219+
fn test_serialize_context_value_text() {
220+
assert_eq!(
221+
serialize_context_value(&ContextValue::Text("hello".into())),
222+
"hello"
223+
);
224+
}
225+
226+
#[test]
227+
fn test_serialize_context_value_number() {
228+
assert_eq!(serialize_context_value(&ContextValue::Number(99.0)), "99");
229+
}
230+
231+
#[test]
232+
fn test_context_value_deserialize_array() {
233+
let v: ContextValue =
234+
serde_json::from_str(r#"["a","b"]"#).expect("should deserialize string list");
235+
assert_eq!(v, ContextValue::StringList(vec!["a".into(), "b".into()]));
236+
}
237+
238+
#[test]
239+
fn test_context_value_deserialize_string() {
240+
let v: ContextValue = serde_json::from_str(r#""hello""#).expect("should deserialize text");
241+
assert_eq!(v, ContextValue::Text("hello".into()));
242+
}
243+
244+
#[test]
245+
fn test_context_value_deserialize_number() {
246+
let v: ContextValue = serde_json::from_str("42").expect("should deserialize number");
247+
assert_eq!(v, ContextValue::Number(42.0));
248+
}
249+
}

crates/common/src/auction/formats.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use serde_json::Value as JsonValue;
1212
use std::collections::HashMap;
1313
use uuid::Uuid;
1414

15+
use crate::auction::context::ContextValue;
1516
use crate::creative;
1617
use crate::error::TrustedServerError;
1718
use crate::geo::GeoInfo;
@@ -30,7 +31,6 @@ use super::types::{
3031
#[serde(rename_all = "camelCase")]
3132
pub struct AdRequest {
3233
pub ad_units: Vec<AdUnit>,
33-
#[allow(dead_code)]
3434
pub config: Option<JsonValue>,
3535
}
3636

@@ -135,6 +135,40 @@ pub fn convert_tsjs_to_auction_request(
135135
geo: GeoInfo::from_request(req),
136136
});
137137

138+
// Forward allowed config entries from the JS request into the context map.
139+
// Only keys listed in `auction.allowed_context_keys` are accepted;
140+
// unrecognised keys are silently dropped to prevent injection of
141+
// arbitrary data by a malicious client payload.
142+
let mut context = HashMap::new();
143+
if let Some(ref config) = body.config {
144+
if let Some(obj) = config.as_object() {
145+
for (key, value) in obj {
146+
if settings.auction.allowed_context_keys.contains(key) {
147+
match serde_json::from_value::<ContextValue>(value.clone()) {
148+
Ok(cv) => {
149+
context.insert(key.clone(), cv);
150+
}
151+
Err(_) => {
152+
log::debug!(
153+
"Auction context: dropping key '{}' with unsupported type",
154+
key
155+
);
156+
}
157+
}
158+
} else {
159+
log::debug!("Auction context: dropping disallowed key '{}'", key);
160+
}
161+
}
162+
if !context.is_empty() {
163+
log::debug!(
164+
"Auction request context: {} entries ({})",
165+
context.len(),
166+
context.keys().cloned().collect::<Vec<_>>().join(", ")
167+
);
168+
}
169+
}
170+
}
171+
138172
Ok(AuctionRequest {
139173
id: Uuid::new_v4().to_string(),
140174
slots,
@@ -152,7 +186,7 @@ pub fn convert_tsjs_to_auction_request(
152186
domain: settings.publisher.domain.clone(),
153187
page: format!("https://{}", settings.publisher.domain),
154188
}),
155-
context: HashMap::new(),
189+
context,
156190
})
157191
}
158192

crates/common/src/auction/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ use crate::settings::Settings;
1111
use std::sync::Arc;
1212

1313
pub mod config;
14+
pub mod context;
1415
pub mod endpoints;
1516
pub mod formats;
1617
pub mod orchestrator;
1718
pub mod provider;
1819
pub mod types;
1920

2021
pub use config::AuctionConfig;
22+
pub use context::{build_url_with_context_params, ContextQueryParams, ContextValue};
2123
pub use orchestrator::AuctionOrchestrator;
2224
pub use provider::AuctionProvider;
2325
pub use types::{

crates/common/src/auction/orchestrator.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ mod tests {
509509
};
510510
use crate::test_support::tests::crate_test_settings_str;
511511
use fastly::Request;
512-
use std::collections::HashMap;
512+
use std::collections::{HashMap, HashSet};
513513

514514
use super::AuctionOrchestrator;
515515

@@ -645,6 +645,7 @@ mod tests {
645645
mediator: None,
646646
timeout_ms: 2000,
647647
creative_store: "creative_store".to_string(),
648+
allowed_context_keys: HashSet::from(["permutive_segments".to_string()]),
648649
};
649650

650651
let orchestrator = AuctionOrchestrator::new(config);

0 commit comments

Comments
 (0)