Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6c4965b
Add PR15 implementation plan: remove fastly from core crate
prk-Jr Apr 14, 2026
f67be31
Move compat conversion fns to adapter, delete core compat.rs
prk-Jr Apr 14, 2026
c2e0776
Move geo_from_fastly from core to adapter platform
prk-Jr Apr 14, 2026
25bcf99
Move BackendConfig from core to adapter backend module
prk-Jr Apr 14, 2026
8303b84
Delete dead backend_name_for_url from adapter backend
prk-Jr Apr 14, 2026
76087e4
Delete legacy FastlyConfigStore and FastlySecretStore from core
prk-Jr Apr 14, 2026
7795e69
Remove fastly::kv_store from core consent module
prk-Jr Apr 14, 2026
abd1f26
Fix consent KV trait design and formatting
prk-Jr Apr 14, 2026
d96dbbe
Move tokio to dev-dependencies in core (test-only usage)
prk-Jr Apr 14, 2026
8f40601
Remove fastly dependency from trusted-server-core
prk-Jr Apr 14, 2026
9a4357c
Remove stale fastly:: references from core doc comments
prk-Jr Apr 14, 2026
46704c0
Apply cargo fmt formatting fixes
prk-Jr Apr 14, 2026
60611fa
Wire consent KV into auction path and remove tokio from core tests
prk-Jr Apr 15, 2026
02639db
Merge feature/edgezero-pr14-entry-point-dual-path into PR15
prk-Jr Apr 16, 2026
d8060d3
Address PR15 review findings: diagnostic regression, header dedup, co…
prk-Jr Apr 21, 2026
3a53f33
Resolve PR15 consent and backend review findings
prk-Jr Apr 23, 2026
54d191b
Merge feature/edgezero-pr14-entry-point-dual-path into feature/edgeze…
prk-Jr Apr 23, 2026
2e128f7
Merge feature/edgezero-pr14-entry-point-dual-path into PR15
prk-Jr Apr 27, 2026
f5b7097
fix cargo fmt
prk-Jr Apr 27, 2026
725ccdc
Resolve PR review suggestions
prk-Jr Apr 27, 2026
50dc61f
Restore EdgeZero consent KV wiring
prk-Jr May 12, 2026
4bb18cb
Merge EdgeZero PR14 updates into PR15 branch
prk-Jr May 12, 2026
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
6 changes: 5 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/trusted-server-adapter-fastly/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ log-fastly = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
trusted-server-core = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }

[dev-dependencies]
bytes = { workspace = true }
edgezero-core = { workspace = true, features = ["test-utils"] }
48 changes: 39 additions & 9 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ async fn dispatch_fallback(
});
}

handle_publisher_request(&state.settings, &state.registry, services, req)
let publisher_services = runtime_services_for_consent_route(&state.settings, services)?;

handle_publisher_request(&state.settings, &state.registry, &publisher_services, req)
.await
.and_then(|pub_response| {
crate::resolve_publisher_response_buffered(
Expand All @@ -241,8 +243,7 @@ async fn dispatch_fallback(
/// mirroring [`crate::http_error_response`] exactly.
///
/// The near-identical function in `main.rs` is intentional: the legacy path
/// uses fastly HTTP types while this path uses `edgezero_core` types. The
/// duplication will be removed when `legacy_main` is deleted in PR 15.
/// uses fastly HTTP types while this path uses `edgezero_core` types.
pub(crate) fn http_error(report: &Report<TrustedServerError>) -> Response {
let root_error = report.current_context();
log::error!("Error occurred: {:?}", report);
Expand Down Expand Up @@ -426,12 +427,7 @@ fn fallback_route_handler(
Box::pin(execute_handler(
state,
ctx,
|state, services, req| async move {
match runtime_services_for_consent_route(&state.settings, &services) {
Ok(consent_services) => dispatch_fallback(&state, &consent_services, req).await,
Err(e) => Err(e),
}
},
|state, services, req| async move { dispatch_fallback(&state, &services, req).await },
))
}
}
Expand Down Expand Up @@ -754,4 +750,38 @@ mod tests {
"router-level 405 bypasses FinalizeResponseMiddleware; main.rs entry-point covers this"
);
}

#[test]
fn edgezero_missing_consent_store_breaks_only_consent_routes() {
let state = app_state_for_settings(settings_with_missing_consent_store());
let router = TrustedServerApp::routes_for_state(&state);

let admin_response =
block_on(router.oneshot(empty_request(Method::POST, "/admin/keys/rotate")));
assert_eq!(
admin_response.status(),
StatusCode::UNAUTHORIZED,
"admin auth behavior should not depend on consent KV availability"
);

let auction_request = request_builder()
.method(Method::POST)
.uri("/auction")
.body(Body::from(r#"{"adUnits":[]}"#))
.expect("should build auction request");
let auction_response = block_on(router.oneshot(auction_request));
assert_eq!(
auction_response.status(),
StatusCode::SERVICE_UNAVAILABLE,
"auction should fail closed when configured consent KV cannot be opened"
);

let publisher_response =
block_on(router.oneshot(empty_request(Method::GET, "/articles/example")));
assert_eq!(
publisher_response.status(),
StatusCode::SERVICE_UNAVAILABLE,
"publisher fallback should fail closed when configured consent KV cannot be opened"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use error_stack::{Report, ResultExt};
use fastly::backend::Backend;
use url::Url;

use crate::error::TrustedServerError;
use trusted_server_core::error::TrustedServerError;

/// Returns the default port for the given scheme (443 for HTTPS, 80 for HTTP).
#[inline]
Expand Down Expand Up @@ -217,10 +217,9 @@ impl<'a> BackendConfig<'a> {

/// Parse an origin URL into its (scheme, host, port) components.
///
/// Centralises URL parsing so that [`from_url`](Self::from_url),
/// [`from_url_with_first_byte_timeout`](Self::from_url_with_first_byte_timeout),
/// and [`backend_name_for_url`](Self::backend_name_for_url) share one
/// code-path.
/// Centralises URL parsing so that [`from_url`](Self::from_url) and
/// [`from_url_with_first_byte_timeout`](Self::from_url_with_first_byte_timeout)
/// share one code-path.
fn parse_origin(
origin_url: &str,
) -> Result<(String, String, Option<u16>), Report<TrustedServerError>> {
Expand Down Expand Up @@ -287,37 +286,6 @@ impl<'a> BackendConfig<'a> {
.first_byte_timeout(first_byte_timeout)
.ensure()
}

/// Compute the backend name that
/// [`from_url_with_first_byte_timeout`](Self::from_url_with_first_byte_timeout)
/// would produce for the given URL and timeout, **without** registering a
/// backend.
///
/// This is useful when callers need the name for mapping purposes (e.g. the
/// auction orchestrator correlating responses to providers) but want the
/// actual registration to happen later with specific settings.
///
/// The `first_byte_timeout` must match the value that will be used at
/// registration time so that the predicted name is correct.
///
/// # Errors
///
/// Returns an error if the URL cannot be parsed or lacks a host.
pub fn backend_name_for_url(
origin_url: &str,
certificate_check: bool,
first_byte_timeout: Duration,
) -> Result<String, Report<TrustedServerError>> {
let (scheme, host, port) = Self::parse_origin(origin_url)?;

let (name, _) = BackendConfig::new(&scheme, &host)
.port(port)
.certificate_check(certificate_check)
.first_byte_timeout(first_byte_timeout)
.compute_name()?;

Ok(name)
}
}

#[cfg(test)]
Expand Down
158 changes: 158 additions & 0 deletions crates/trusted-server-adapter-fastly/src/compat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//! Compatibility bridge between `fastly` SDK types and `http` crate types.
//!
//! Contains only the functions used by the legacy `main()` entry point.
//! Relocated from `trusted-server-core` as part of removing all `fastly` crate
//! imports from the core library.

use edgezero_core::body::Body as EdgeBody;
use edgezero_core::http::{Request as HttpRequest, RequestBuilder, Response as HttpResponse, Uri};
use trusted_server_core::http_util::SPOOFABLE_FORWARDED_HEADERS;

fn build_http_request(req: &fastly::Request, body: EdgeBody) -> HttpRequest {
let uri: Uri = req
.get_url_str()
.parse()
.expect("should parse fastly request URL as URI");

let mut builder: RequestBuilder = edgezero_core::http::request_builder()
.method(req.get_method().clone())
.uri(uri);

for (name, value) in req.get_headers() {
builder = builder.header(name.as_str(), value.as_bytes());
}

builder
.body(body)
.expect("should build http request from fastly request")
}

/// Convert an owned `fastly::Request` into an [`HttpRequest`].
///
/// # Panics
///
/// Panics if the Fastly request URL cannot be parsed as an `http::Uri`.
pub(crate) fn from_fastly_request(mut req: fastly::Request) -> HttpRequest {
let body = EdgeBody::from(req.take_body_bytes());
build_http_request(&req, body)
}

/// Convert a `fastly::Response` into an [`HttpResponse`].
pub(crate) fn from_fastly_response(mut resp: fastly::Response) -> HttpResponse {
let status = resp.get_status();
let mut builder = edgezero_core::http::response_builder().status(status);
for (name, value) in resp.get_headers() {
builder = builder.header(name.as_str(), value.as_bytes());
}
builder
.body(EdgeBody::from(resp.take_body_bytes()))
.expect("should build http response from fastly response")
}

/// Convert an [`HttpResponse`] into a `fastly::Response`.
pub(crate) fn to_fastly_response(resp: HttpResponse) -> fastly::Response {
let (parts, body) = resp.into_parts();
let mut fastly_resp = fastly::Response::from_status(parts.status.as_u16());
for (name, value) in &parts.headers {
fastly_resp.append_header(name.as_str(), value.as_bytes());
}

match body {
EdgeBody::Once(bytes) => {
if !bytes.is_empty() {
fastly_resp.set_body(bytes.to_vec());
}
}
EdgeBody::Stream(_) => {
log::warn!("streaming body in compat::to_fastly_response; body will be empty");
}
}

fastly_resp
}

/// Convert an [`HttpResponse`] into a `fastly::Response` without a body.
///
/// Use this when the caller will stream the body separately through
/// [`fastly::Response::stream_to_client`].
pub(crate) fn to_fastly_response_skeleton(resp: HttpResponse) -> fastly::Response {
let (parts, _body) = resp.into_parts();
let mut fastly_resp = fastly::Response::from_status(parts.status.as_u16());
for (name, value) in &parts.headers {
fastly_resp.append_header(name.as_str(), value.as_bytes());
}
fastly_resp
}

/// Sanitize forwarded headers on a `fastly::Request`.
///
/// Strips headers that clients can spoof before any request-derived context
/// is built or the request is converted to core HTTP types.
pub(crate) fn sanitize_fastly_forwarded_headers(req: &mut fastly::Request) {
for &name in SPOOFABLE_FORWARDED_HEADERS {
if req.get_header(name).is_some() {
log::debug!("Stripped spoofable header: {name}");
req.remove_header(name);
}
}
}
Comment thread
prk-Jr marked this conversation as resolved.

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

#[test]
fn sanitize_fastly_forwarded_headers_strips_spoofable() {
let mut req = fastly::Request::get("https://example.com/");
req.set_header("forwarded", "for=1.2.3.4");
req.set_header("x-forwarded-host", "evil.example.com");
req.set_header("x-forwarded-proto", "http");
req.set_header("fastly-ssl", "1");
req.set_header("host", "example.com");

sanitize_fastly_forwarded_headers(&mut req);

assert!(
req.get_header("forwarded").is_none(),
"should strip forwarded"
);
assert!(
req.get_header("x-forwarded-host").is_none(),
"should strip x-forwarded-host"
);
assert!(
req.get_header("x-forwarded-proto").is_none(),
"should strip x-forwarded-proto"
);
assert!(
req.get_header("fastly-ssl").is_none(),
"should strip fastly-ssl"
);
assert!(req.get_header("host").is_some(), "should preserve host");
}

#[test]
fn to_fastly_response_with_streaming_body_produces_empty_body() {
use edgezero_core::http::StatusCode;

let stream = futures::stream::empty::<bytes::Bytes>();
let stream_body = EdgeBody::stream(stream);

let http_resp = edgezero_core::http::response_builder()
.status(StatusCode::OK)
.body(stream_body)
.expect("should build response");

let mut fastly_resp = to_fastly_response(http_resp);

assert_eq!(
fastly_resp.get_status().as_u16(),
200,
"should preserve status"
);
assert!(
fastly_resp.take_body_bytes().is_empty(),
"should produce empty body for streaming response"
);
}
}
10 changes: 5 additions & 5 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use fastly::{Request as FastlyRequest, Response as FastlyResponse};
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::AuctionOrchestrator;
use trusted_server_core::auth::enforce_basic_auth;
use trusted_server_core::compat;
use trusted_server_core::error::{IntoHttpResponse, TrustedServerError};
use trusted_server_core::geo::GeoInfo;
use trusted_server_core::integrations::IntegrationRegistry;
Expand All @@ -37,6 +36,8 @@ use trusted_server_core::settings::Settings;
use trusted_server_core::settings_data::get_settings;

mod app;
mod backend;
mod compat;
mod error;
mod logging;
mod management_api;
Expand Down Expand Up @@ -236,10 +237,9 @@ fn apply_entry_point_finalize<F>(
/// Preserves identical semantics to the pre-PR14 `main()`. Called when
/// the `edgezero_enabled` config flag is absent or `false`.
///
/// The thin fastly<->http conversion layer (via `compat::from_fastly_request` /
/// `compat::to_fastly_response`) lives here in the adapter crate. `compat.rs`
/// will be deleted in PR 15 once this legacy path is retired.
// TODO: delete after Phase 5 EdgeZero cutover - see issue #495
/// The thin fastly↔http conversion layer (via `compat::from_fastly_request` /
/// `compat::to_fastly_response`) lives here in the adapter crate.
// TODO: delete after Phase 5 EdgeZero cutover — see issue #495
fn legacy_main(mut req: FastlyRequest) {
let state = match build_state() {
Ok(state) => state,
Expand Down
3 changes: 1 addition & 2 deletions crates/trusted-server-adapter-fastly/src/management_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use fastly::http::{Method, StatusCode};
use fastly::{Request, Response};
use trusted_server_core::platform::{PlatformError, PlatformSecretStore, StoreName};

use crate::backend::BackendConfig;
use crate::platform::FastlyPlatformSecretStore;

const FASTLY_API_HOST: &str = "https://api.fastly.com";
Expand Down Expand Up @@ -122,8 +123,6 @@ impl FastlyManagementApiClient {
/// be registered, or [`PlatformError::SecretStore`] if the API key cannot
/// be read.
pub(crate) fn new() -> Result<Self, Report<PlatformError>> {
use trusted_server_core::backend::BackendConfig;

let backend_name = BackendConfig::from_url(FASTLY_API_HOST, true)
.change_context(PlatformError::Backend)
.attach("failed to register Fastly management API backend")?;
Expand Down
Loading
Loading