Skip to content
Open
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
163 changes: 94 additions & 69 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use edgezero_core::body::Body as EdgeBody;
use edgezero_core::http::{
header, HeaderName, HeaderValue, Method, Request as HttpRequest, Response as HttpResponse,
};
use error_stack::Report;
use fastly::http::Method;
use fastly::{Error, Request, Response};
use fastly::http::Method as FastlyMethod;
use fastly::{Error, Request as FastlyRequest, Response as FastlyResponse};

use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{build_orchestrator, AuctionOrchestrator};
Expand All @@ -10,7 +14,7 @@ use trusted_server_core::constants::{
ENV_FASTLY_IS_STAGING, ENV_FASTLY_SERVICE_VERSION, HEADER_X_GEO_INFO_AVAILABLE,
HEADER_X_TS_ENV, HEADER_X_TS_VERSION,
};
use trusted_server_core::error::TrustedServerError;
use trusted_server_core::error::{IntoHttpResponse, TrustedServerError};
use trusted_server_core::geo::GeoInfo;
use trusted_server_core::integrations::IntegrationRegistry;
use trusted_server_core::platform::RuntimeServices;
Expand All @@ -35,13 +39,13 @@ use crate::error::to_error_response;
use crate::platform::{build_runtime_services, open_kv_store, UnavailableKvStore};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
fn main(mut req: FastlyRequest) -> Result<FastlyResponse, Error> {
logging::init_logger();

// Keep the health probe independent from settings loading and routing so
// readiness checks still get a cheap liveness response during startup.
if req.get_method() == Method::GET && req.get_path() == "/health" {
return Ok(Response::from_status(200).with_body_text_plain("ok"));
if req.get_method() == FastlyMethod::GET && req.get_path() == "/health" {
return Ok(FastlyResponse::from_status(200).with_body_text_plain("ok"));
}

let settings = match get_settings() {
Expand Down Expand Up @@ -85,64 +89,61 @@ fn main(req: Request) -> Result<Response, Error> {
as std::sync::Arc<dyn trusted_server_core::platform::PlatformKvStore>
}
};
// Strip client-spoofable forwarded headers at the edge before building
// any request-derived context or converting to the core HTTP types.
compat::sanitize_fastly_forwarded_headers(&mut req);

let runtime_services = build_runtime_services(&req, kv_store);
let http_req = compat::from_fastly_request(req);

futures::executor::block_on(route_request(
let mut response = futures::executor::block_on(route_request(
&settings,
&orchestrator,
&integration_registry,
&runtime_services,
Comment thread
prk-Jr marked this conversation as resolved.
req,
http_req,
))
.unwrap_or_else(|e| http_error_response(&e));

let geo_info = if response.status() == edgezero_core::http::StatusCode::UNAUTHORIZED {
None
} else {
runtime_services
.geo()
.lookup(runtime_services.client_info().client_ip)
.unwrap_or_else(|e| {
log::warn!("geo lookup failed: {e}");
None
})
};

finalize_response(&settings, geo_info.as_ref(), &mut response);

Ok(compat::to_fastly_response(response))
}

async fn route_request(
settings: &Settings,
orchestrator: &AuctionOrchestrator,
integration_registry: &IntegrationRegistry,
runtime_services: &RuntimeServices,
mut req: Request,
) -> Result<Response, Error> {
// Strip client-spoofable forwarded headers at the edge.
// On Fastly this service IS the first proxy — these headers from
// clients are untrusted and can hijack URL rewriting (see #409).
compat::sanitize_fastly_forwarded_headers(&mut req);

// Look up geo info via the platform abstraction using the client IP
// already captured in RuntimeServices at the entry point.
let geo_info = runtime_services
.geo()
.lookup(runtime_services.client_info().client_ip)
.unwrap_or_else(|e| {
log::warn!("geo lookup failed: {e}");
None
});

req: HttpRequest,
) -> Result<HttpResponse, Report<TrustedServerError>> {
// `get_settings()` should already have rejected invalid handler regexes.
// Keep this fallback so manually-constructed or otherwise unprepared
// settings still become an error response instead of panicking.
let auth_req = compat::from_fastly_headers_ref(&req);
match enforce_basic_auth(settings, &auth_req) {
Ok(Some(response)) => {
let mut response = compat::to_fastly_response(response);
finalize_response(settings, geo_info.as_ref(), &mut response);
return Ok(response);
}
match enforce_basic_auth(settings, &req) {
Ok(Some(response)) => return Ok(response),
Ok(None) => {}
Err(e) => {
log::error!("Failed to evaluate basic auth: {:?}", e);
let mut response = to_error_response(&e);
finalize_response(settings, geo_info.as_ref(), &mut response);
return Ok(response);
}
Err(e) => return Err(e),
}

// Get path and method for routing
let path = req.get_path().to_string();
let method = req.get_method().clone();
let path = req.uri().path().to_string();
let method = req.method().clone();

// Match known routes and handle them
let result = match (method, path.as_str()) {
match (method, path.as_str()) {
// Serve the tsjs library
(Method::GET, path) if path.starts_with("/static/tsjs=") => {
handle_tsjs_dynamic(&req, integration_registry)
Expand Down Expand Up @@ -183,14 +184,24 @@ async fn route_request(
(Method::POST, "/first-party/proxy-rebuild") => {
handle_first_party_proxy_rebuild(settings, runtime_services, req).await
}
(m, path) if integration_registry.has_route(&m, path) => integration_registry
.handle_proxy(&m, path, settings, runtime_services, req)
.await
.unwrap_or_else(|| {
Err(Report::new(TrustedServerError::BadRequest {
message: format!("Unknown integration route: {path}"),
}))
}),
(m, path) if integration_registry.has_route(&m, path) => {
// TODO(PR13): migrate integration trait to http types here
integration_registry
.handle_proxy(
&m,
path,
settings,
runtime_services,
compat::to_fastly_request(req),
)
.await
.unwrap_or_else(|| {
Err(Report::new(TrustedServerError::BadRequest {
message: format!("Unknown integration route: {path}"),
}))
})
.map(compat::from_fastly_response)
}

// No known route matched, proxy to publisher origin as fallback
_ => {
Expand All @@ -199,22 +210,9 @@ async fn route_request(
path
);

match handle_publisher_request(settings, integration_registry, runtime_services, req) {
Ok(response) => Ok(response),
Err(e) => {
log::error!("Failed to proxy to publisher origin: {:?}", e);
Err(e)
}
}
handle_publisher_request(settings, integration_registry, runtime_services, req).await
}
};

// Convert any errors to HTTP error responses
let mut response = result.unwrap_or_else(|e| to_error_response(&e));

finalize_response(settings, geo_info.as_ref(), &mut response);

Ok(response)
}
}

/// Applies all standard response headers: geo, version, staging, and configured headers.
Expand All @@ -225,21 +223,48 @@ async fn route_request(
/// Header precedence (last write wins): geo headers are set first, then
/// version/staging, then operator-configured `settings.response_headers`.
/// This means operators can intentionally override any managed header.
fn finalize_response(settings: &Settings, geo_info: Option<&GeoInfo>, response: &mut Response) {
fn finalize_response(settings: &Settings, geo_info: Option<&GeoInfo>, response: &mut HttpResponse) {
if let Some(geo) = geo_info {
geo.set_response_headers(response);
} else {
response.set_header(HEADER_X_GEO_INFO_AVAILABLE, "false");
response.headers_mut().insert(
HEADER_X_GEO_INFO_AVAILABLE,
HeaderValue::from_static("false"),
);
}

if let Ok(v) = ::std::env::var(ENV_FASTLY_SERVICE_VERSION) {
response.set_header(HEADER_X_TS_VERSION, v);
if let Ok(value) = HeaderValue::from_str(&v) {
response.headers_mut().insert(HEADER_X_TS_VERSION, value);
} else {
log::warn!("Skipping invalid FASTLY_SERVICE_VERSION response header value");
}
}
if ::std::env::var(ENV_FASTLY_IS_STAGING).as_deref() == Ok("1") {
response.set_header(HEADER_X_TS_ENV, "staging");
response
.headers_mut()
.insert(HEADER_X_TS_ENV, HeaderValue::from_static("staging"));
}

for (key, value) in &settings.response_headers {
response.set_header(key, value);
let header_name = HeaderName::from_bytes(key.as_bytes())
.expect("settings.response_headers validated at load time");
let header_value =
HeaderValue::from_str(value).expect("settings.response_headers validated at load time");
response.headers_mut().insert(header_name, header_value);
}
}

fn http_error_response(report: &Report<TrustedServerError>) -> HttpResponse {
let root_error = report.current_context();
log::error!("Error occurred: {:?}", report);

Comment thread
prk-Jr marked this conversation as resolved.
let mut response =
HttpResponse::new(EdgeBody::from(format!("{}\n", root_error.user_message())));
*response.status_mut() = root_error.status_code();
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
response
}
30 changes: 17 additions & 13 deletions crates/trusted-server-core/src/auction/endpoints.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! HTTP endpoint handlers for auction requests.

use edgezero_core::body::Body as EdgeBody;
use error_stack::{Report, ResultExt};
use fastly::{Request, Response};
use http::{Request, Response};

use crate::auction::formats::AdRequest;
use crate::compat;
Expand Down Expand Up @@ -33,21 +34,22 @@ pub async fn handle_auction(
settings: &Settings,
orchestrator: &AuctionOrchestrator,
services: &RuntimeServices,
mut req: Request,
) -> Result<Response, Report<TrustedServerError>> {
req: Request<EdgeBody>,
) -> Result<Response<EdgeBody>, Report<TrustedServerError>> {
let (parts, body) = req.into_parts();

// Parse request body
let body: AdRequest = serde_json::from_slice(&req.take_body_bytes()).change_context(
TrustedServerError::Auction {
let body: AdRequest =
serde_json::from_slice(&body.into_bytes()).change_context(TrustedServerError::Auction {
message: "Failed to parse auction request body".to_string(),
},
)?;
})?;
Comment thread
prk-Jr marked this conversation as resolved.

log::info!(
"Auction request received for {} ad units",
body.ad_units.len()
);

let http_req = compat::from_fastly_headers_ref(&req);
let http_req = Request::from_parts(parts, EdgeBody::empty());

// Generate synthetic ID early so the consent pipeline can use it for
// KV Store fallback/write operations.
Expand All @@ -57,11 +59,10 @@ pub async fn handle_auction(
},
)?;

// Extract consent from request cookies, headers, and geo.
let cookie_jar = handle_request_cookies(&http_req)?;
let geo = services
.geo()
.lookup(services.client_info.client_ip)
.lookup(services.client_info().client_ip)
.unwrap_or_else(|e| {
log::warn!("geo lookup failed: {e}");
None
Expand All @@ -79,17 +80,20 @@ pub async fn handle_auction(
&body,
settings,
services,
&req,
&http_req,
consent_context,
&synthetic_id,
geo,
)?;

// Body already parsed above; provider context only needs request metadata.
let fastly_req = compat::to_fastly_request_ref(&http_req);
Comment thread
prk-Jr marked this conversation as resolved.

// Create auction context
let context = AuctionContext {
settings,
request: &req,
client_info: &services.client_info,
request: &fastly_req,
client_info: services.client_info(),
timeout_ms: settings.auction.timeout_ms,
provider_responses: None,
services,
Expand Down
Loading
Loading