Skip to content

Commit d478b69

Browse files
Address S3 and Image Optimizer review findings
1 parent 462bdf1 commit d478b69

5 files changed

Lines changed: 210 additions & 32 deletions

File tree

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use trusted_server_core::integrations::IntegrationRegistry;
1616
use trusted_server_core::platform::RuntimeServices;
1717
use trusted_server_core::proxy::{
1818
handle_asset_proxy_request, handle_first_party_click, handle_first_party_proxy,
19-
handle_first_party_proxy_rebuild, handle_first_party_proxy_sign,
19+
handle_first_party_proxy_rebuild, handle_first_party_proxy_sign, AssetProxyCachePolicy,
2020
};
2121
use trusted_server_core::publisher::{
2222
handle_publisher_request, handle_tsjs_dynamic, stream_publisher_body, PublisherResponse,
@@ -207,7 +207,7 @@ async fn route_request(
207207
let path = req.get_path().to_string();
208208
let method = req.get_method().clone();
209209

210-
let mut preserve_no_store_private = false;
210+
let mut asset_cache_policy = AssetProxyCachePolicy::OriginControlled;
211211

212212
// Match known routes and handle them
213213
let result = match (method, path.as_str()) {
@@ -280,10 +280,9 @@ async fn route_request(
280280
);
281281
match handle_asset_proxy_request(settings, runtime_services, req, asset_route).await
282282
{
283-
Ok(response) => {
284-
preserve_no_store_private = response.get_header_str(header::CACHE_CONTROL)
285-
== Some("no-store, private");
286-
Ok(response)
283+
Ok(asset_response) => {
284+
asset_cache_policy = asset_response.cache_policy();
285+
Ok(asset_response.into_response())
287286
}
288287
Err(e) => Err(e),
289288
}
@@ -351,9 +350,7 @@ async fn route_request(
351350
let mut response = result.unwrap_or_else(|e| to_error_response(&e));
352351

353352
finalize_response(settings, geo_info.as_ref(), &mut response);
354-
if preserve_no_store_private {
355-
response.set_header(header::CACHE_CONTROL, "no-store, private");
356-
}
353+
asset_cache_policy.apply_after_route_finalization(&mut response);
357354

358355
Some(response)
359356
}

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,14 @@ pub(crate) fn options_for_asset_request(
7575
let profile_params = profile_set
7676
.profiles
7777
.get(selected_profile)
78-
.expect("should select only configured image optimizer profiles");
78+
.ok_or_else(|| {
79+
Report::new(TrustedServerError::Configuration {
80+
message: format!(
81+
"image_optimizer.profile_sets `{}` selected profile `{selected_profile}` is not defined",
82+
route_config.profile_set
83+
),
84+
})
85+
})?;
7986
params.merge_from(parse_param_string(profile_params)?);
8087

8188
apply_aspect_ratio_override(profile_set, selected_profile, query, &mut params);
@@ -362,7 +369,8 @@ fn parse_aspect_ratio_value(value: &str) -> Option<(u32, u32)> {
362369
mod tests {
363370
use super::*;
364371
use crate::settings::{
365-
ImageOptimizerAspectRatioConfig, ImageOptimizerCropOffsetsConfig, ImageOptimizerProfileSet,
372+
AssetImageOptimizerConfig, ImageOptimizerAspectRatioConfig,
373+
ImageOptimizerCropOffsetsConfig, ImageOptimizerProfileSet, ImageOptimizerSettings,
366374
};
367375

368376
fn profile_set() -> ImageOptimizerProfileSet {
@@ -392,6 +400,34 @@ mod tests {
392400
}
393401
}
394402

403+
#[test]
404+
fn options_for_asset_request_returns_error_when_selected_profile_is_missing() {
405+
let mut settings = crate::test_support::tests::create_test_settings();
406+
let mut profile_set = profile_set();
407+
profile_set.profiles.remove("default");
408+
settings.image_optimizer = ImageOptimizerSettings {
409+
profile_sets: std::collections::HashMap::from([(
410+
"default_images".to_string(),
411+
profile_set,
412+
)]),
413+
};
414+
let mut route = ProxyAssetRoute::new("/.images/", "https://assets.example.com");
415+
route.image_optimizer = Some(AssetImageOptimizerConfig {
416+
enabled: true,
417+
region: "us_east".to_string(),
418+
profile_set: "default_images".to_string(),
419+
origin_query: None,
420+
});
421+
422+
let err = options_for_asset_request(&settings, &route, "")
423+
.expect_err("should return a configuration error for broken profile sets");
424+
425+
assert!(
426+
format!("{err:?}").contains("selected profile `default` is not defined"),
427+
"should describe the missing selected profile without panicking: {err:?}"
428+
);
429+
}
430+
395431
#[test]
396432
fn profile_conversion_adds_aspect_ratio_and_smart_crop() {
397433
let set = profile_set();

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

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use crate::error::TrustedServerError;
2222
use crate::platform::{
2323
PlatformBackendSpec, PlatformHttpRequest, PlatformResponse, RuntimeServices, StoreName,
2424
};
25+
use crate::redacted::Redacted;
2526
use crate::s3_sigv4::{self, S3Credentials};
2627
use crate::settings::{
2728
AssetOriginAuth, OriginQueryPolicy, ProxyAssetRoute, S3SigV4AuthConfig, Settings,
@@ -69,6 +70,66 @@ const ASSET_PROXY_FORWARD_HEADERS: [header::HeaderName; 11] = [
6970
const ASSET_PROXY_STRIP_RESPONSE_HEADERS: [&str; 3] =
7071
["set-cookie", "strict-transport-security", "clear-site-data"];
7172

73+
/// Cache-control value used when asset proxy responses must not be stored.
74+
pub const ASSET_NO_STORE_PRIVATE_CACHE_CONTROL: &str = "no-store, private";
75+
76+
/// Cache policy metadata emitted by the asset proxy handler.
77+
///
78+
/// The Fastly router finalizes standard response headers after the asset handler
79+
/// returns. This typed policy lets the router reapply protected cache directives
80+
/// without depending on an exact header string set by the handler.
81+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82+
pub enum AssetProxyCachePolicy {
83+
/// Leave origin/cache-control headers under normal response finalization.
84+
OriginControlled,
85+
/// Reapply `Cache-Control: no-store, private` after standard finalization.
86+
NoStorePrivate,
87+
}
88+
89+
impl AssetProxyCachePolicy {
90+
/// Apply protected cache headers after route-level response finalization.
91+
pub fn apply_after_route_finalization(self, response: &mut Response) {
92+
if self == Self::NoStorePrivate {
93+
apply_no_store_cache_control(response);
94+
}
95+
}
96+
}
97+
98+
/// Asset proxy response plus metadata needed by the outer router.
99+
pub struct AssetProxyResponse {
100+
response: Response,
101+
cache_policy: AssetProxyCachePolicy,
102+
}
103+
104+
impl AssetProxyResponse {
105+
fn new(response: Response, cache_policy: AssetProxyCachePolicy) -> Self {
106+
Self {
107+
response,
108+
cache_policy,
109+
}
110+
}
111+
112+
fn origin_controlled(response: Response) -> Self {
113+
Self::new(response, AssetProxyCachePolicy::OriginControlled)
114+
}
115+
116+
fn no_store_private(response: Response) -> Self {
117+
Self::new(response, AssetProxyCachePolicy::NoStorePrivate)
118+
}
119+
120+
/// Return cache policy metadata for router finalization.
121+
#[must_use]
122+
pub fn cache_policy(&self) -> AssetProxyCachePolicy {
123+
self.cache_policy
124+
}
125+
126+
/// Consume this wrapper and return the Fastly response.
127+
#[must_use]
128+
pub fn into_response(self) -> Response {
129+
self.response
130+
}
131+
}
132+
72133
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
73134
struct S3CredentialsCacheKey {
74135
secret_store: String,
@@ -684,8 +745,8 @@ fn load_s3_credentials(
684745
.transpose()?;
685746
let credentials = Arc::new(S3Credentials {
686747
access_key_id,
687-
secret_access_key,
688-
session_token,
748+
secret_access_key: Redacted::new(secret_access_key),
749+
session_token: session_token.map(Redacted::new),
689750
});
690751

691752
let mut cache = S3_CREDENTIALS_CACHE
@@ -792,7 +853,7 @@ fn strip_asset_proxy_response_headers(response: &mut Response) {
792853
fn apply_no_store_cache_control(response: &mut Response) {
793854
response.set_header(
794855
header::CACHE_CONTROL,
795-
HeaderValue::from_static("no-store, private"),
856+
HeaderValue::from_static(ASSET_NO_STORE_PRIVATE_CACHE_CONTROL),
796857
);
797858
}
798859

@@ -813,7 +874,7 @@ async fn preflight_s3_origin_for_image_optimizer(
813874
request_method: &Method,
814875
unsigned_headers: &http::HeaderMap,
815876
backend_name: &str,
816-
) -> Result<Option<Response>, Report<TrustedServerError>> {
877+
) -> Result<Option<AssetProxyResponse>, Report<TrustedServerError>> {
817878
let Some(auth @ AssetOriginAuth::S3SigV4(_)) = route.auth.as_ref() else {
818879
return Ok(None);
819880
};
@@ -841,7 +902,7 @@ async fn preflight_s3_origin_for_image_optimizer(
841902
let mut response = head_response;
842903
strip_asset_proxy_response_headers(&mut response);
843904
apply_no_store_cache_control(&mut response);
844-
return Ok(Some(response));
905+
return Ok(Some(AssetProxyResponse::no_store_private(response)));
845906
}
846907

847908
let mut get_headers = unsigned_headers.clone();
@@ -858,7 +919,7 @@ async fn preflight_s3_origin_for_image_optimizer(
858919
strip_asset_proxy_response_headers(&mut response);
859920
apply_no_store_cache_control(&mut response);
860921

861-
Ok(Some(response))
922+
Ok(Some(AssetProxyResponse::no_store_private(response)))
862923
}
863924

864925
/// Proxy a configured first-party asset path to its matched asset origin.
@@ -882,7 +943,7 @@ pub async fn handle_asset_proxy_request(
882943
services: &RuntimeServices,
883944
req: Request,
884945
route: &ProxyAssetRoute,
885-
) -> Result<Response, Report<TrustedServerError>> {
946+
) -> Result<AssetProxyResponse, Report<TrustedServerError>> {
886947
let incoming_query = req.get_query_str().unwrap_or("");
887948
let mut target_url = build_asset_proxy_target_url(route, req.get_path(), incoming_query)?;
888949
let image_optimizer =
@@ -967,7 +1028,7 @@ pub async fn handle_asset_proxy_request(
9671028
let mut response = platform_response_to_fastly_asset(platform_resp).await?;
9681029
strip_asset_proxy_response_headers(&mut response);
9691030

970-
Ok(response)
1031+
Ok(AssetProxyResponse::origin_controlled(response))
9711032
}
9721033

9731034
/// Upserts the `ts-ec` query parameter on a URL, replacing any existing value.
@@ -1731,8 +1792,8 @@ mod tests {
17311792
clear_s3_credentials_cache_for_tests, handle_asset_proxy_request, handle_first_party_click,
17321793
handle_first_party_proxy, handle_first_party_proxy_rebuild, handle_first_party_proxy_sign,
17331794
is_host_allowed, proxy_request, rebuild_response_with_body,
1734-
reconstruct_and_validate_signed_target, redirect_is_permitted, ProxyRequestConfig,
1735-
SUPPORTED_ENCODINGS,
1795+
reconstruct_and_validate_signed_target, redirect_is_permitted, AssetProxyCachePolicy,
1796+
ProxyRequestConfig, SUPPORTED_ENCODINGS,
17361797
};
17371798
use crate::constants::{HEADER_ACCEPT, HEADER_X_FORWARDED_FOR};
17381799
use crate::creative;
@@ -2761,7 +2822,8 @@ mod tests {
27612822
let route = ProxyAssetRoute::new("/.images/", "https://assets.example.com:8443");
27622823
let response = handle_asset_proxy_request(&settings, &services, req, &route)
27632824
.await
2764-
.expect("should proxy asset request");
2825+
.expect("should proxy asset request")
2826+
.into_response();
27652827
assert_eq!(response.get_status(), StatusCode::OK);
27662828

27672829
let all_headers = stub.recorded_request_headers();
@@ -2852,7 +2914,8 @@ mod tests {
28522914
let route = ProxyAssetRoute::new("/.images/", "https://assets.example.com");
28532915
let response = handle_asset_proxy_request(&settings, &services, req, &route)
28542916
.await
2855-
.expect("should proxy asset request");
2917+
.expect("should proxy asset request")
2918+
.into_response();
28562919

28572920
assert!(
28582921
response.get_header(header::SET_COOKIE).is_none(),
@@ -3152,7 +3215,8 @@ mod tests {
31523215

31533216
let mut response = handle_asset_proxy_request(&settings, &services, req, &route)
31543217
.await
3155-
.expect("should proxy optimized S3 asset request");
3218+
.expect("should proxy optimized S3 asset request")
3219+
.into_response();
31563220

31573221
assert_eq!(response.get_status(), StatusCode::OK);
31583222
assert_eq!(response.take_body_str(), "optimized");
@@ -3213,9 +3277,15 @@ mod tests {
32133277
);
32143278
let route = test_s3_image_optimizer_route();
32153279

3216-
let mut response = handle_asset_proxy_request(&settings, &services, req, &route)
3280+
let asset_response = handle_asset_proxy_request(&settings, &services, req, &route)
32173281
.await
32183282
.expect("should return raw S3 error response");
3283+
assert_eq!(
3284+
asset_response.cache_policy(),
3285+
AssetProxyCachePolicy::NoStorePrivate,
3286+
"should carry a typed no-store policy for router finalization"
3287+
);
3288+
let mut response = asset_response.into_response();
32193289

32203290
assert_eq!(response.get_status(), StatusCode::NOT_FOUND);
32213291
assert_eq!(
@@ -3271,7 +3341,8 @@ mod tests {
32713341

32723342
let mut response = handle_asset_proxy_request(&settings, &services, req, &route)
32733343
.await
3274-
.expect("should proxy debug S3 asset request");
3344+
.expect("should proxy debug S3 asset request")
3345+
.into_response();
32753346

32763347
assert_eq!(response.take_body_str(), "raw");
32773348
assert_eq!(
@@ -3336,7 +3407,8 @@ mod tests {
33363407

33373408
let mut response = handle_asset_proxy_request(&settings, &services, req, &route)
33383409
.await
3339-
.expect("should proxy a streaming asset response");
3410+
.expect("should proxy a streaming asset response")
3411+
.into_response();
33403412

33413413
assert_eq!(response.take_body_str(), "chunk");
33423414
}

0 commit comments

Comments
 (0)