Skip to content
Draft
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
51 changes: 4 additions & 47 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/trusted-server-adapter-fastly/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ log-fastly = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
trusted-server-core = { path = "../trusted-server-core" }
url = { workspace = true }
urlencoding = { workspace = true }
trusted-server-js = { path = "../js" }

Expand Down
53 changes: 46 additions & 7 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ use trusted_server_core::settings_data::get_settings;
use crate::middleware::{AuthMiddleware, FinalizeResponseMiddleware};
use crate::platform::open_kv_store;
use crate::platform::{
FastlyPlatformBackend, FastlyPlatformConfigStore, FastlyPlatformGeo, FastlyPlatformHttpClient,
FastlyPlatformSecretStore, UnavailableKvStore,
FastlyConsentKvStore, FastlyPlatformBackend, FastlyPlatformConfigStore, FastlyPlatformGeo,
FastlyPlatformHttpClient, FastlyPlatformSecretStore, UnavailableKvStore,
};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -126,6 +126,16 @@ fn build_per_request_services(state: &AppState, ctx: &RequestContext) -> Runtime
.build()
}

/// Open the consent KV store named in `config`, returning `None` when not configured or unavailable.
pub(crate) fn open_consent_kv(
config: &trusted_server_core::consent_config::ConsentConfig,
) -> Option<FastlyConsentKvStore> {
config
.consent_store
.as_deref()
.and_then(FastlyConsentKvStore::open)
}

// ---------------------------------------------------------------------------
// Error helper
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -227,9 +237,18 @@ impl Hooks for TrustedServerApp {
async move {
let services = build_per_request_services(&s, &ctx);
let req = ctx.into_request();
Ok(handle_auction(&s.settings, &s.orchestrator, &services, req)
.await
.unwrap_or_else(|e| http_error(&e)))
let consent_kv = open_consent_kv(&s.settings.consent);
Ok(handle_auction(
&s.settings,
&s.orchestrator,
&services,
consent_kv
.as_ref()
.map(|kv| kv as &dyn trusted_server_core::consent::kv::ConsentKvOps),
req,
)
.await
.unwrap_or_else(|e| http_error(&e)))
}
};

Expand Down Expand Up @@ -322,7 +341,17 @@ impl Hooks for TrustedServerApp {
}))
})
} else {
handle_publisher_request(&s.settings, &s.registry, &services, req).await
let consent_kv = open_consent_kv(&s.settings.consent);
handle_publisher_request(
&s.settings,
&s.registry,
&services,
consent_kv
.as_ref()
.map(|kv| kv as &dyn trusted_server_core::consent::kv::ConsentKvOps),
req,
)
.await
};

Ok(result.unwrap_or_else(|e| http_error(&e)))
Expand All @@ -349,7 +378,17 @@ impl Hooks for TrustedServerApp {
}))
})
} else {
handle_publisher_request(&s.settings, &s.registry, &services, req).await
let consent_kv = open_consent_kv(&s.settings.consent);
handle_publisher_request(
&s.settings,
&s.registry,
&services,
consent_kv
.as_ref()
.map(|kv| kv as &dyn trusted_server_core::consent::kv::ConsentKvOps),
req,
)
.await
};

Ok(result.unwrap_or_else(|e| http_error(&e)))
Expand Down
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
82 changes: 82 additions & 0 deletions crates/trusted-server-adapter-fastly/src/compat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! Compatibility bridge between `fastly` SDK types and `http` crate types.
//!
//! Contains only the three functions used by the legacy `main()` entry point.
//! Relocated from `trusted-server-core` in PR 15 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};

/// Forwarded headers that clients can inject to spoof request context.
///
/// Inlined from `trusted_server_core::http_util` which is `pub(crate)`.
const SPOOFABLE_FORWARDED_HEADERS: &[&str] = &[
"forwarded",
"x-forwarded-host",
"x-forwarded-proto",
"fastly-ssl",
];

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 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
}

/// 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);
}
}
}
33 changes: 29 additions & 4 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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};
use trusted_server_core::auth::enforce_basic_auth;
use trusted_server_core::compat;
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,
Expand All @@ -31,13 +30,15 @@ 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;
mod middleware;
mod platform;

use crate::app::TrustedServerApp;
use crate::app::{open_consent_kv, TrustedServerApp};
use crate::error::to_error_response;
use crate::platform::{build_runtime_services, open_kv_store, UnavailableKvStore};
use edgezero_core::app::Hooks as _;
Expand Down Expand Up @@ -228,7 +229,17 @@ async fn route_request(

// Unified auction endpoint (returns creative HTML inline)
(Method::POST, "/auction") => {
handle_auction(settings, orchestrator, runtime_services, req).await
let consent_kv = open_consent_kv(&settings.consent);
handle_auction(
settings,
orchestrator,
runtime_services,
consent_kv
.as_ref()
.map(|kv| kv as &dyn trusted_server_core::consent::kv::ConsentKvOps),
req,
)
.await
}

// tsjs endpoints
Expand Down Expand Up @@ -260,7 +271,21 @@ async fn route_request(
path
);

handle_publisher_request(settings, integration_registry, runtime_services, req).await
let consent_kv = settings
.consent
.consent_store
.as_deref()
.and_then(crate::platform::FastlyConsentKvStore::open);
handle_publisher_request(
settings,
integration_registry,
runtime_services,
consent_kv
.as_ref()
.map(|kv| kv as &dyn trusted_server_core::consent::kv::ConsentKvOps),
req,
)
.await
}
}
}
Expand Down
Loading
Loading