Skip to content

Commit 3dc1262

Browse files
Add tester cookie clear endpoint (#797)
1 parent 5b0a413 commit 3dc1262

8 files changed

Lines changed: 254 additions & 12 deletions

File tree

crates/trusted-server-adapter-fastly/src/app.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
//! | POST | `/_ts/api/v1/batch-sync` | [`handle_batch_sync`] |
2525
//! | GET | `/_ts/api/v1/identify` | [`handle_identify`] |
2626
//! | GET | `/_ts/set-tester` | [`handle_set_tester`] |
27+
//! | GET | `/_ts/clear-tester` | [`handle_clear_tester`] |
2728
//! | OPTIONS | `/_ts/api/v1/identify` | [`cors_preflight_identify`] |
2829
//! | POST | `/auction` | [`handle_auction`] |
2930
//! | GET | `/first-party/proxy` | [`handle_first_party_proxy`] |
@@ -120,7 +121,7 @@ use trusted_server_core::request_signing::{
120121
};
121122
use trusted_server_core::settings::{ProxyAssetRoute, Settings};
122123
use trusted_server_core::settings_data::get_settings;
123-
use trusted_server_core::tester_cookie::handle_set_tester;
124+
use trusted_server_core::tester_cookie::{handle_clear_tester, handle_set_tester};
124125

125126
use crate::middleware::{AuthMiddleware, FinalizeResponseMiddleware};
126127
use crate::platform::{
@@ -552,6 +553,7 @@ async fn run_named_route(
552553
}
553554
}
554555
NamedRouteHandler::SetTester => handle_set_tester(&state.settings),
556+
NamedRouteHandler::ClearTester => handle_clear_tester(&state.settings),
555557
NamedRouteHandler::Auction => {
556558
// The auction reads consent data, so the consent KV store must be
557559
// available — fail closed with 503 when it is configured but
@@ -909,6 +911,7 @@ enum NamedRouteHandler {
909911
BatchSync,
910912
Identify,
911913
SetTester,
914+
ClearTester,
912915
Auction,
913916
FirstPartyProxy,
914917
FirstPartyClick,
@@ -971,6 +974,11 @@ const NAMED_ROUTES: &[NamedRoute] = &[
971974
primary_methods: &[Method::GET],
972975
handler: NamedRouteHandler::SetTester,
973976
},
977+
NamedRoute {
978+
path: "/_ts/clear-tester",
979+
primary_methods: &[Method::GET],
980+
handler: NamedRouteHandler::ClearTester,
981+
},
974982
NamedRoute {
975983
path: "/auction",
976984
primary_methods: &[Method::POST],
@@ -1529,6 +1537,48 @@ mod tests {
15291537
);
15301538
}
15311539

1540+
#[test]
1541+
fn dispatch_clear_tester_is_disabled_by_default() {
1542+
let router = test_router();
1543+
let response = block_on(router.oneshot(empty_request(Method::GET, "/_ts/clear-tester")));
1544+
1545+
assert_eq!(
1546+
response.status(),
1547+
StatusCode::NOT_FOUND,
1548+
"disabled clear tester-cookie route should return 404"
1549+
);
1550+
assert!(
1551+
response.headers().get(header::SET_COOKIE).is_none(),
1552+
"disabled clear tester-cookie route should not set a cookie"
1553+
);
1554+
}
1555+
1556+
#[test]
1557+
fn dispatch_clear_tester_clears_cookie_on_configured_domain() {
1558+
let mut settings = test_settings();
1559+
settings.tester_cookie.enabled = true;
1560+
let state = app_state_for_settings(settings);
1561+
let router = TrustedServerApp::routes_for_state(&state);
1562+
let response = block_on(router.oneshot(empty_request(Method::GET, "/_ts/clear-tester")));
1563+
1564+
assert_eq!(
1565+
response.status(),
1566+
StatusCode::NO_CONTENT,
1567+
"enabled clear tester-cookie route should return no content"
1568+
);
1569+
let set_cookie = response
1570+
.headers()
1571+
.get(header::SET_COOKIE)
1572+
.expect("should clear tester cookie")
1573+
.to_str()
1574+
.expect("should render set-cookie as utf-8");
1575+
assert_eq!(
1576+
set_cookie,
1577+
"ts-tester=; Domain=.test-publisher.com; Path=/; Secure; SameSite=Lax; Max-Age=0",
1578+
"tester cookie clear should use publisher.cookie_domain"
1579+
);
1580+
}
1581+
15321582
#[test]
15331583
fn dispatch_fallback_attaches_ec_finalize_state() {
15341584
// The publisher fallback must thread EC finalize state to the entry

crates/trusted-server-adapter-fastly/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use trusted_server_core::request_signing::{
5151
};
5252
use trusted_server_core::settings::Settings;
5353
use trusted_server_core::settings_data::get_settings;
54-
use trusted_server_core::tester_cookie::handle_set_tester;
54+
use trusted_server_core::tester_cookie::{handle_clear_tester, handle_set_tester};
5555

5656
mod app;
5757
mod backend;
@@ -866,6 +866,7 @@ async fn route_request(
866866
(outcome, false)
867867
}
868868
(Method::GET, "/_ts/set-tester") => (handle_set_tester(settings), false),
869+
(Method::GET, "/_ts/clear-tester") => (handle_clear_tester(settings), false),
869870

870871
// Unified auction endpoint.
871872
(Method::POST, "/auction") => {

crates/trusted-server-adapter-fastly/src/route_tests.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,63 @@ fn set_tester_route_sets_cookie_on_configured_domain_when_enabled() {
10811081
);
10821082
}
10831083

1084+
#[test]
1085+
fn clear_tester_route_is_disabled_by_default() {
1086+
let settings = create_test_settings();
1087+
let (orchestrator, integration_registry) = build_route_stack(&settings);
1088+
let req = Request::get("https://test.com/_ts/clear-tester");
1089+
let services = test_runtime_services(&req);
1090+
1091+
let response = route_buffered_response(
1092+
&settings,
1093+
&orchestrator,
1094+
&integration_registry,
1095+
&services,
1096+
req,
1097+
"should route disabled clear tester-cookie request",
1098+
);
1099+
1100+
assert_eq!(
1101+
response.get_status(),
1102+
StatusCode::NOT_FOUND,
1103+
"disabled clear tester-cookie route should return 404"
1104+
);
1105+
assert_eq!(
1106+
response.get_header_str(header::SET_COOKIE),
1107+
None,
1108+
"disabled clear tester-cookie route should not set a cookie"
1109+
);
1110+
}
1111+
1112+
#[test]
1113+
fn clear_tester_route_clears_cookie_on_configured_domain_when_enabled() {
1114+
let mut settings = create_test_settings();
1115+
settings.tester_cookie.enabled = true;
1116+
let (orchestrator, integration_registry) = build_route_stack(&settings);
1117+
let req = Request::get("https://test.com/_ts/clear-tester");
1118+
let services = test_runtime_services(&req);
1119+
1120+
let response = route_buffered_response(
1121+
&settings,
1122+
&orchestrator,
1123+
&integration_registry,
1124+
&services,
1125+
req,
1126+
"should route enabled clear tester-cookie request",
1127+
);
1128+
1129+
assert_eq!(
1130+
response.get_status(),
1131+
StatusCode::NO_CONTENT,
1132+
"enabled clear tester-cookie route should return no content"
1133+
);
1134+
assert_eq!(
1135+
response.get_header_str(header::SET_COOKIE),
1136+
Some("ts-tester=; Domain=.test-publisher.com; Path=/; Secure; SameSite=Lax; Max-Age=0"),
1137+
"tester cookie clear should use publisher.cookie_domain"
1138+
);
1139+
}
1140+
10841141
#[test]
10851142
fn malformed_auction_json_returns_bad_request() {
10861143
let settings = create_auction_test_settings(r#"["prebid"]"#);

crates/trusted-server-core/src/settings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1702,7 +1702,7 @@ pub struct DebugConfig {
17021702
/// Tester-cookie endpoint configuration.
17031703
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
17041704
pub struct TesterCookieConfig {
1705-
/// Enable `GET /_ts/set-tester`, which sets `ts-tester=true`.
1705+
/// Enable tester-cookie endpoints that set and clear `ts-tester`.
17061706
#[serde(default)]
17071707
pub enabled: bool,
17081708
}

crates/trusted-server-core/src/tester_cookie.rs

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Tester-cookie endpoint helpers.
22
//!
3-
//! The tester route is intentionally disabled unless configured. When enabled,
4-
//! it sets a first-party `ts-tester=true` cookie scoped to the configured
3+
//! The tester routes are intentionally disabled unless configured. When enabled,
4+
//! they set or clear a first-party `ts-tester` cookie scoped to the configured
55
//! publisher cookie domain.
66
77
use edgezero_core::body::Body as EdgeBody;
@@ -20,6 +20,14 @@ fn format_tester_cookie(domain: &str) -> String {
2020
)
2121
}
2222

23+
/// Formats the tester cookie clearing `Set-Cookie` header value.
24+
fn format_clear_tester_cookie(domain: &str) -> String {
25+
format!(
26+
"{}=; Domain={}; Path=/; Secure; SameSite=Lax; Max-Age=0",
27+
COOKIE_TS_TESTER, domain,
28+
)
29+
}
30+
2331
/// Handles `GET /_ts/set-tester`.
2432
///
2533
/// Returns `404 Not Found` while `[tester_cookie].enabled` is false. When the
@@ -57,6 +65,44 @@ pub fn handle_set_tester(
5765
Ok(response)
5866
}
5967

68+
/// Handles `GET /_ts/clear-tester`.
69+
///
70+
/// Returns `404 Not Found` while `[tester_cookie].enabled` is false. When the
71+
/// feature is enabled, returns `204 No Content` with `Set-Cookie` expiring the
72+
/// `ts-tester` cookie scoped to `publisher.cookie_domain`.
73+
///
74+
/// # Errors
75+
///
76+
/// Returns [`TrustedServerError::InvalidHeaderValue`] if the configured cookie
77+
/// domain cannot be rendered as an HTTP header value.
78+
pub fn handle_clear_tester(
79+
settings: &Settings,
80+
) -> Result<Response<EdgeBody>, Report<TrustedServerError>> {
81+
if !settings.tester_cookie.enabled {
82+
let mut response = Response::new(EdgeBody::empty());
83+
*response.status_mut() = StatusCode::NOT_FOUND;
84+
return Ok(response);
85+
}
86+
87+
let set_cookie = HeaderValue::from_str(&format_clear_tester_cookie(
88+
&settings.publisher.cookie_domain,
89+
))
90+
.change_context(TrustedServerError::InvalidHeaderValue {
91+
message: "tester cookie clear contains invalid header value".to_string(),
92+
})?;
93+
94+
let mut response = Response::new(EdgeBody::empty());
95+
*response.status_mut() = StatusCode::NO_CONTENT;
96+
response.headers_mut().insert(
97+
header::CACHE_CONTROL,
98+
HeaderValue::from_static("no-store, private"),
99+
);
100+
response
101+
.headers_mut()
102+
.insert(header::SET_COOKIE, set_cookie);
103+
Ok(response)
104+
}
105+
60106
#[cfg(test)]
61107
mod tests {
62108
use super::*;
@@ -111,4 +157,56 @@ mod tests {
111157
"disabled tester route should not set a cookie"
112158
);
113159
}
160+
161+
#[test]
162+
fn clear_tester_cookie_uses_configured_cookie_domain() {
163+
let mut settings = create_test_settings();
164+
settings.tester_cookie.enabled = true;
165+
settings.publisher.cookie_domain = ".tester.example".to_string();
166+
167+
let response = handle_clear_tester(&settings).expect("should build clear tester response");
168+
169+
assert_eq!(
170+
response.status(),
171+
StatusCode::NO_CONTENT,
172+
"enabled clear tester route should return no content"
173+
);
174+
assert_eq!(
175+
response
176+
.headers()
177+
.get(header::CACHE_CONTROL)
178+
.and_then(|value| value.to_str().ok()),
179+
Some("no-store, private"),
180+
"clear tester route should not be cacheable"
181+
);
182+
let set_cookie = response
183+
.headers()
184+
.get(header::SET_COOKIE)
185+
.expect("should clear tester cookie")
186+
.to_str()
187+
.expect("should render set-cookie as utf-8");
188+
assert_eq!(
189+
set_cookie,
190+
"ts-tester=; Domain=.tester.example; Path=/; Secure; SameSite=Lax; Max-Age=0",
191+
"tester cookie clear should use publisher.cookie_domain"
192+
);
193+
}
194+
195+
#[test]
196+
fn clear_tester_cookie_route_is_disabled_by_default() {
197+
let settings = create_test_settings();
198+
199+
let response =
200+
handle_clear_tester(&settings).expect("should build disabled clear tester response");
201+
202+
assert_eq!(
203+
response.status(),
204+
StatusCode::NOT_FOUND,
205+
"disabled clear tester route should return not found"
206+
);
207+
assert!(
208+
response.headers().get(header::SET_COOKIE).is_none(),
209+
"disabled clear tester route should not set a cookie"
210+
);
211+
}
114212
}

docs/guide/api-reference.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,35 @@ The cookie domain comes from `[publisher].cookie_domain`.
4949
curl -i "https://edge.example.com/_ts/set-tester"
5050
```
5151

52+
### GET /\_ts/clear-tester
53+
54+
Clears the first-party tester marker cookie for QA or troubleshooting workflows.
55+
56+
**Configuration:** Uses the same `[tester_cookie].enabled` flag as `/_ts/set-tester`.
57+
58+
**Response when enabled:**
59+
60+
- **Status:** `204 No Content`
61+
- **Headers:**
62+
63+
```http
64+
Set-Cookie: ts-tester=; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax; Max-Age=0
65+
Cache-Control: no-store, private
66+
```
67+
68+
The cookie domain comes from `[publisher].cookie_domain`.
69+
70+
**Response when disabled:**
71+
72+
- **Status:** `404 Not Found`
73+
- **Set-Cookie:** none
74+
75+
**Example:**
76+
77+
```bash
78+
curl -i "https://edge.example.com/_ts/clear-tester"
79+
```
80+
5281
---
5382

5483
## First-Party Endpoints

docs/guide/configuration.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,14 +326,14 @@ TRUSTED_SERVER__PUBLISHER__MAX_BUFFERED_BODY_BYTES=16777216
326326

327327
## Tester Cookie Configuration
328328

329-
Settings for the optional tester-cookie endpoint. This feature is disabled by
329+
Settings for the optional tester-cookie endpoints. This feature is disabled by
330330
default and should only be enabled for intentional QA or troubleshooting flows.
331331

332332
### `[tester_cookie]`
333333

334-
| Field | Type | Required | Description |
335-
| --------- | ------- | -------- | ------------------------------------------------------------ |
336-
| `enabled` | Boolean | No | Enables `GET /_ts/set-tester` to set `ts-tester=true` cookie |
334+
| Field | Type | Required | Description |
335+
| --------- | ------- | -------- | -------------------------------------------------- |
336+
| `enabled` | Boolean | No | Enables routes to set and clear `ts-tester` cookie |
337337

338338
When enabled, `GET /_ts/set-tester` returns `204 No Content` and sets:
339339

@@ -342,7 +342,14 @@ Set-Cookie: ts-tester=true; Domain=<publisher.cookie_domain>; Path=/; Secure; Sa
342342
Cache-Control: no-store, private
343343
```
344344

345-
When disabled, the route returns `404 Not Found` and does not set a cookie.
345+
`GET /_ts/clear-tester` returns `204 No Content` and clears the cookie:
346+
347+
```http
348+
Set-Cookie: ts-tester=; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax; Max-Age=0
349+
Cache-Control: no-store, private
350+
```
351+
352+
When disabled, both routes return `404 Not Found` and do not set a cookie.
346353

347354
::: warning
348355
The cookie is scoped with `[publisher].cookie_domain`, not the EC-specific

trusted-server.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ proxy_secret = "change-me-proxy-secret"
2121
# Raise it for deployments serving larger publisher pages:
2222
# max_buffered_body_bytes = 16777216 # 16 MiB
2323

24-
# Tester-cookie endpoint. When enabled, GET /_ts/set-tester sets
25-
# ts-tester=true on publisher.cookie_domain.
24+
# Tester-cookie endpoints. When enabled, GET /_ts/set-tester sets
25+
# ts-tester=true and GET /_ts/clear-tester clears it on publisher.cookie_domain.
2626
[tester_cookie]
2727
enabled = false
2828

0 commit comments

Comments
 (0)