Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
52 changes: 5 additions & 47 deletions 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 @@ -20,8 +20,10 @@ 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" }

[dev-dependencies]
bytes = { workspace = true }
edgezero-core = { workspace = true, features = ["test-utils"] }
29 changes: 24 additions & 5 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ use trusted_server_core::settings::Settings;
use trusted_server_core::settings_data::get_settings;

use crate::middleware::{AuthMiddleware, FinalizeResponseMiddleware};
use crate::platform::open_kv_store;
use crate::platform::{open_consent_kv, open_kv_store};
use crate::platform::{
FastlyPlatformBackend, FastlyPlatformConfigStore, FastlyPlatformGeo, FastlyPlatformHttpClient,
FastlyPlatformSecretStore, UnavailableKvStore,
Expand Down Expand Up @@ -193,7 +193,17 @@ async fn dispatch_fallback(
});
}

handle_publisher_request(&state.settings, &state.registry, services, req).await
let consent_kv = open_consent_kv(&state.settings.consent);
handle_publisher_request(
&state.settings,
&state.registry,
services,
consent_kv
.as_ref()
.map(|kv| kv as &dyn trusted_server_core::consent::kv::ConsentKvOps),
req,
)
.await
}

// ---------------------------------------------------------------------------
Expand All @@ -204,8 +214,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 @@ -312,7 +321,17 @@ impl Hooks for TrustedServerApp {
let auction_handler = move |ctx: RequestContext| {
let s = Arc::clone(&s);
execute_handler(s, ctx, |state, services, req| async move {
handle_auction(&state.settings, &state.orchestrator, &services, req).await
let consent_kv = open_consent_kv(&state.settings.consent);
handle_auction(
&state.settings,
&state.orchestrator,
&services,
consent_kv
.as_ref()
.map(|kv| kv as &dyn trusted_server_core::consent::kv::ConsentKvOps),
req,
)
.await
})
};

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
133 changes: 133 additions & 0 deletions crates/trusted-server-adapter-fastly/src/compat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! 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` 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 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);
}
}
}
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"
);
}
}
Loading
Loading