perf(app-proxy): preconnect the media origin and revalidate the app shell (#1827)

This commit is contained in:
Hampus
2026-08-24 12:26:21 +02:00
committed by GitHub
parent 991be1c5a2
commit 82eb87bc47
5 changed files with 1427 additions and 50 deletions
+2
View File
@@ -7,6 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content, maximum-scale=1, user-scalable=no">
<meta name="description" content="Fluxer is a free and open source instant messaging and VoIP chat app built for friends, groups, and communities."> <meta name="description" content="Fluxer is a free and open source instant messaging and VoIP chat app built for friends, groups, and communities.">
<link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}"> <link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}">
<link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}" crossorigin>
<link rel="preconnect" href="{{MEDIA_ENDPOINT}}">
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" sizes="32x32" href="{{STATIC_CDN_ENDPOINT}}/web/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="{{STATIC_CDN_ENDPOINT}}/web/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="{{STATIC_CDN_ENDPOINT}}/web/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="{{STATIC_CDN_ENDPOINT}}/web/apple-touch-icon.png">
+178 -8
View File
@@ -67,17 +67,26 @@ pub fn build_bootstrap_script(
) )
} }
const MEDIA_PRECONNECT_TAG: &str = r#"<link rel="preconnect" href="{{MEDIA_ENDPOINT}}">"#;
const STATIC_PRECONNECT_TAGS: [&str; 2] = [
r#"<link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}">"#,
r#"<link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}" crossorigin>"#,
];
pub fn inject_bootstrap( pub fn inject_bootstrap(
html: &str, html: &str,
nonce: &str, nonce: &str,
script_tag: &str, script_tag: &str,
static_cdn_endpoint: &str, static_cdn_endpoint: &str,
media_endpoint: &str,
) -> String { ) -> String {
let static_cdn = static_cdn_endpoint.trim_end_matches('/');
let media = media_endpoint.trim_end_matches('/');
let nonced = html.replace("{{CSP_NONCE_PLACEHOLDER}}", nonce); let nonced = html.replace("{{CSP_NONCE_PLACEHOLDER}}", nonce);
let nonced = nonced.replace( let nonced = apply_static_preconnect(&nonced, static_cdn);
"{{STATIC_CDN_ENDPOINT}}", let nonced = nonced.replace("{{STATIC_CDN_ENDPOINT}}", static_cdn);
static_cdn_endpoint.trim_end_matches('/'), let nonced = apply_media_preconnect(&nonced, media, static_cdn);
);
if nonced.contains("<!--{{FLUXER_BOOTSTRAP}}-->") { if nonced.contains("<!--{{FLUXER_BOOTSTRAP}}-->") {
return nonced.replace("<!--{{FLUXER_BOOTSTRAP}}-->", script_tag); return nonced.replace("<!--{{FLUXER_BOOTSTRAP}}-->", script_tag);
@@ -111,6 +120,27 @@ pub fn inject_bootstrap(
nonced nonced
} }
fn apply_static_preconnect(html: &str, static_cdn: &str) -> String {
if !static_cdn.is_empty() {
return html.to_owned();
}
let mut stripped = html.to_owned();
for tag in STATIC_PRECONNECT_TAGS {
stripped = stripped.replace(&format!("{tag}\n"), "").replace(tag, "");
}
stripped
}
fn apply_media_preconnect(html: &str, media: &str, static_cdn: &str) -> String {
if media.is_empty() || media == static_cdn {
return html
.replace(&format!("{MEDIA_PRECONNECT_TAG}\n"), "")
.replace(MEDIA_PRECONNECT_TAG, "")
.replace("{{MEDIA_ENDPOINT}}", "");
}
html.replace("{{MEDIA_ENDPOINT}}", media)
}
fn escape_json_for_script(value: &str) -> String { fn escape_json_for_script(value: &str) -> String {
value value
.replace("</", "<\\/") .replace("</", "<\\/")
@@ -122,10 +152,58 @@ fn escape_json_for_script(value: &str) -> String {
mod tests { mod tests {
use super::*; use super::*;
const SHIPPED_APP_SHELL: &str = include_str!("../../fluxer_app/index.html");
fn inject_into_shipped_shell() -> String {
inject_bootstrap(
SHIPPED_APP_SHELL,
"shellnonce",
"<script>boot</script>",
"https://cdn.example.test/",
"https://media.example.test/",
)
}
#[test]
fn shipped_shell_declares_both_static_cdn_socket_pools() {
assert!(
SHIPPED_APP_SHELL
.contains(r#"<link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}" crossorigin>"#),
"fluxer_app/index.html has no anonymous preconnect for its module scripts and fonts"
);
assert!(
SHIPPED_APP_SHELL.contains(r#"<link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}">"#),
"fluxer_app/index.html lost the credentialed preconnect its stylesheet and icons use"
);
}
#[test]
fn shipped_shell_serves_three_distinct_preconnects() {
let result = inject_into_shipped_shell();
assert!(result.contains(r#"<link rel="preconnect" href="https://cdn.example.test">"#));
assert!(
result
.contains(r#"<link rel="preconnect" href="https://cdn.example.test" crossorigin>"#)
);
assert!(result.contains(r#"<link rel="preconnect" href="https://media.example.test">"#));
assert_eq!(result.matches("preconnect").count(), 3);
}
#[test]
fn shipped_shell_ships_no_unsubstituted_placeholder() {
let result = inject_into_shipped_shell();
assert!(!result.contains("{{STATIC_CDN_ENDPOINT}}"));
assert!(!result.contains("{{MEDIA_ENDPOINT}}"));
assert!(!result.contains("{{CSP_NONCE_PLACEHOLDER}}"));
assert!(!result.contains("{{FLUXER_BOOTSTRAP}}"));
assert!(result.contains("<script>boot</script>"));
assert!(result.contains(r#"nonce="shellnonce""#));
}
#[test] #[test]
fn inject_bootstrap_before_head_close() { fn inject_bootstrap_before_head_close() {
let html = "<html><head><title>App</title></head><body></body></html>"; let html = "<html><head><title>App</title></head><body></body></html>";
let result = inject_bootstrap(html, "abc123", "<script>boot</script>", ""); let result = inject_bootstrap(html, "abc123", "<script>boot</script>", "", "");
assert!(result.contains("<script>boot</script>")); assert!(result.contains("<script>boot</script>"));
assert!(result.contains("<head>")); assert!(result.contains("<head>"));
} }
@@ -133,7 +211,7 @@ mod tests {
#[test] #[test]
fn inject_bootstrap_fluxer_placeholder() { fn inject_bootstrap_fluxer_placeholder() {
let html = "<html><head>{{FLUXER_BOOTSTRAP}}</head></html>"; let html = "<html><head>{{FLUXER_BOOTSTRAP}}</head></html>";
let result = inject_bootstrap(html, "n1", "<script>x</script>", ""); let result = inject_bootstrap(html, "n1", "<script>x</script>", "", "");
assert!(result.contains("<script>x</script>")); assert!(result.contains("<script>x</script>"));
assert!(!result.contains("{{FLUXER_BOOTSTRAP}}")); assert!(!result.contains("{{FLUXER_BOOTSTRAP}}"));
} }
@@ -141,7 +219,7 @@ mod tests {
#[test] #[test]
fn inject_bootstrap_comment_placeholder() { fn inject_bootstrap_comment_placeholder() {
let html = "<html><head><!--{{FLUXER_BOOTSTRAP}}--></head></html>"; let html = "<html><head><!--{{FLUXER_BOOTSTRAP}}--></head></html>";
let result = inject_bootstrap(html, "n2", "<script>y</script>", ""); let result = inject_bootstrap(html, "n2", "<script>y</script>", "", "");
assert!(result.contains("<script>y</script>")); assert!(result.contains("<script>y</script>"));
assert!(!result.contains("<!--{{FLUXER_BOOTSTRAP}}-->")); assert!(!result.contains("<!--{{FLUXER_BOOTSTRAP}}-->"));
} }
@@ -149,7 +227,7 @@ mod tests {
#[test] #[test]
fn inject_bootstrap_replaces_csp_nonce_placeholder() { fn inject_bootstrap_replaces_csp_nonce_placeholder() {
let html = r#"<html><head><script nonce="{{CSP_NONCE_PLACEHOLDER}}"></script>{{FLUXER_BOOTSTRAP}}</head></html>"#; let html = r#"<html><head><script nonce="{{CSP_NONCE_PLACEHOLDER}}"></script>{{FLUXER_BOOTSTRAP}}</head></html>"#;
let result = inject_bootstrap(html, "mynonce", "<script>z</script>", ""); let result = inject_bootstrap(html, "mynonce", "<script>z</script>", "", "");
assert!(result.contains(r#"nonce="mynonce""#)); assert!(result.contains(r#"nonce="mynonce""#));
assert!(!result.contains("{{CSP_NONCE_PLACEHOLDER}}")); assert!(!result.contains("{{CSP_NONCE_PLACEHOLDER}}"));
} }
@@ -162,11 +240,103 @@ mod tests {
"nonce", "nonce",
"<script>boot</script>", "<script>boot</script>",
"https://cdn.example.test/", "https://cdn.example.test/",
"",
); );
assert!(result.contains(r#"href="https://cdn.example.test/web/favicon-32x32.png""#)); assert!(result.contains(r#"href="https://cdn.example.test/web/favicon-32x32.png""#));
assert!(!result.contains("{{STATIC_CDN_ENDPOINT}}")); assert!(!result.contains("{{STATIC_CDN_ENDPOINT}}"));
} }
#[test]
fn inject_bootstrap_replaces_media_endpoint_placeholder() {
let html = r#"<html><head><link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}">
<link rel="preconnect" href="{{MEDIA_ENDPOINT}}">
{{FLUXER_BOOTSTRAP}}</head></html>"#;
let result = inject_bootstrap(
html,
"nonce",
"<script>boot</script>",
"https://cdn.example.test/",
"https://media.example.test/",
);
assert!(result.contains(r#"<link rel="preconnect" href="https://media.example.test">"#));
assert!(result.contains(r#"<link rel="preconnect" href="https://cdn.example.test">"#));
assert!(!result.contains("{{MEDIA_ENDPOINT}}"));
}
#[test]
fn inject_bootstrap_drops_media_preconnect_when_endpoint_is_empty() {
let html = r#"<html><head><link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}">
<link rel="preconnect" href="{{MEDIA_ENDPOINT}}">
{{FLUXER_BOOTSTRAP}}</head></html>"#;
let result = inject_bootstrap(
html,
"nonce",
"<script>boot</script>",
"https://cdn.example.test",
"",
);
assert_eq!(result.matches("preconnect").count(), 1);
assert!(!result.contains("{{MEDIA_ENDPOINT}}"));
assert!(!result.contains(r#"href="">"#));
}
#[test]
fn inject_bootstrap_drops_media_preconnect_when_it_matches_the_static_cdn() {
let html = r#"<html><head><link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}">
<link rel="preconnect" href="{{MEDIA_ENDPOINT}}">
{{FLUXER_BOOTSTRAP}}</head></html>"#;
let result = inject_bootstrap(
html,
"nonce",
"<script>boot</script>",
"https://cdn.example.test",
"https://cdn.example.test/",
);
assert_eq!(result.matches("preconnect").count(), 1);
assert!(!result.contains("{{MEDIA_ENDPOINT}}"));
}
const SHELL_PRECONNECT_HEAD: &str = r#"<html><head><link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}">
<link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}" crossorigin>
<link rel="preconnect" href="{{MEDIA_ENDPOINT}}">
{{FLUXER_BOOTSTRAP}}</head></html>"#;
#[test]
fn static_cdn_keeps_a_credentialed_and_an_anonymous_preconnect() {
let result = inject_bootstrap(
SHELL_PRECONNECT_HEAD,
"nonce",
"<script>boot</script>",
"https://cdn.example.test/",
"https://media.example.test",
);
assert!(result.contains(r#"<link rel="preconnect" href="https://cdn.example.test">"#));
assert!(
result
.contains(r#"<link rel="preconnect" href="https://cdn.example.test" crossorigin>"#)
);
assert_eq!(result.matches("preconnect").count(), 3);
}
#[test]
fn both_static_preconnects_are_dropped_when_the_endpoint_is_empty() {
let result = inject_bootstrap(
SHELL_PRECONNECT_HEAD,
"nonce",
"<script>boot</script>",
"",
"https://media.example.test",
);
assert_eq!(result.matches("preconnect").count(), 1);
assert!(!result.contains(r#"href="""#));
assert!(!result.contains("{{STATIC_CDN_ENDPOINT}}"));
}
#[test]
fn media_preconnect_carries_no_crossorigin_attribute() {
assert!(!MEDIA_PRECONNECT_TAG.contains("crossorigin"));
}
#[test] #[test]
fn escape_json_for_script_escapes_closing_script() { fn escape_json_for_script_escapes_closing_script() {
assert_eq!(escape_json_for_script("</script>"), "<\\/script>"); assert_eq!(escape_json_for_script("</script>"), "<\\/script>");
+493 -15
View File
@@ -11,10 +11,18 @@ use axum::{
use std::path::Path as FsPath; use std::path::Path as FsPath;
use std::time::Duration; use std::time::Duration;
use super::spa_static::{CORS_ALLOW_ANY_VALUE, guess_mime, is_font_mime, is_hashed_asset}; use super::spa_static::{CORS_ALLOW_ANY_VALUE, asset_cache_control, guess_mime, is_font_mime};
const ASSET_REQUEST_TIMEOUT: Duration = Duration::from_secs(15); const ASSET_REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
const MAX_ASSET_SIZE_BYTES: u64 = 100 * 1024 * 1024; const MAX_ASSET_SIZE_BYTES: u64 = 100 * 1024 * 1024;
const UPSTREAM_FAILURE_CACHE_CONTROL: &str = "no-store";
const UPSTREAM_FAILURE_STRIPPED_HEADERS: &[&str] = &[
"cdn-cache-control",
"cloudflare-cdn-cache-control",
"surrogate-control",
"expires",
"age",
];
const BLOCKED_REQUEST_HEADERS: &[&str] = &[ const BLOCKED_REQUEST_HEADERS: &[&str] = &[
"accept-encoding", "accept-encoding",
@@ -50,7 +58,12 @@ pub async fn proxy_assets(
request: axum::extract::Request, request: axum::extract::Request,
) -> Response { ) -> Response {
let Some(cdn_endpoint) = &state.config.static_cdn_endpoint else { let Some(cdn_endpoint) = &state.config.static_cdn_endpoint else {
return serve_local_asset(&state.config.static_dir, &format!("assets/{path}")).await; return serve_local_asset(
&state.config.static_dir,
&format!("assets/{path}"),
request.headers(),
)
.await;
}; };
let target_url = format!("{cdn_endpoint}/assets/{path}"); let target_url = format!("{cdn_endpoint}/assets/{path}");
@@ -95,7 +108,8 @@ pub async fn proxy_assets(
return StatusCode::PAYLOAD_TOO_LARGE.into_response(); return StatusCode::PAYLOAD_TOO_LARGE.into_response();
} }
let status = upstream_response.status(); let status = StatusCode::from_u16(upstream_response.status().as_u16())
.unwrap_or(StatusCode::BAD_GATEWAY);
let mut response_headers = axum::http::HeaderMap::new(); let mut response_headers = axum::http::HeaderMap::new();
for (name, value) in upstream_response.headers() { for (name, value) in upstream_response.headers() {
@@ -110,6 +124,7 @@ pub async fn proxy_assets(
} }
set_known_asset_content_type(&mut response_headers, &path); set_known_asset_content_type(&mut response_headers, &path);
set_font_cors(&mut response_headers); set_font_cors(&mut response_headers);
set_proxied_cache_control(&mut response_headers, &path, status);
let asset_csp = build_asset_csp( let asset_csp = build_asset_csp(
&state.config.csp, &state.config.csp,
@@ -128,13 +143,16 @@ pub async fn proxy_assets(
let body = Body::from_stream(upstream_response.bytes_stream()); let body = Body::from_stream(upstream_response.bytes_stream());
let mut response = Response::new(body); let mut response = Response::new(body);
*response.status_mut() = *response.status_mut() = status;
StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
*response.headers_mut() = response_headers; *response.headers_mut() = response_headers;
response response
} }
async fn serve_local_asset(static_dir: &str, relative_path: &str) -> Response { async fn serve_local_asset(
static_dir: &str,
relative_path: &str,
request_headers: &HeaderMap,
) -> Response {
let file_path = FsPath::new(static_dir).join(relative_path); let file_path = FsPath::new(static_dir).join(relative_path);
let resolved = match tokio::fs::canonicalize(&file_path).await { let resolved = match tokio::fs::canonicalize(&file_path).await {
@@ -150,6 +168,19 @@ async fn serve_local_asset(static_dir: &str, relative_path: &str) -> Response {
return StatusCode::NOT_FOUND.into_response(); return StatusCode::NOT_FOUND.into_response();
} }
let entity_tag = tokio::fs::metadata(&resolved)
.await
.ok()
.and_then(|metadata| local_asset_entity_tag(&metadata));
if let Some(entity_tag) = entity_tag.as_deref()
&& if_none_match_matches(request_headers, entity_tag)
{
let mut response = StatusCode::NOT_MODIFIED.into_response();
set_local_asset_headers(response.headers_mut(), relative_path, Some(entity_tag));
return response;
}
let content = match tokio::fs::read(&resolved).await { let content = match tokio::fs::read(&resolved).await {
Ok(bytes) => bytes, Ok(bytes) => bytes,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => { Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
@@ -166,22 +197,65 @@ async fn serve_local_asset(static_dir: &str, relative_path: &str) -> Response {
if let Ok(value) = HeaderValue::from_str(mime_type) { if let Ok(value) = HeaderValue::from_str(mime_type) {
response.headers_mut().insert(header::CONTENT_TYPE, value); response.headers_mut().insert(header::CONTENT_TYPE, value);
} }
if is_font_mime(mime_type) { set_local_asset_headers(response.headers_mut(), relative_path, entity_tag.as_deref());
response.headers_mut().insert( response
}
fn set_local_asset_headers(headers: &mut HeaderMap, relative_path: &str, entity_tag: Option<&str>) {
headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_static(asset_cache_control(relative_path)),
);
if is_font_mime(guess_mime(relative_path)) {
headers.insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN, header::ACCESS_CONTROL_ALLOW_ORIGIN,
HeaderValue::from_static(CORS_ALLOW_ANY_VALUE), HeaderValue::from_static(CORS_ALLOW_ANY_VALUE),
); );
} }
let cache_control = if is_hashed_asset(relative_path) { if let Some(entity_tag) = entity_tag
"public, max-age=31536000, immutable" && let Ok(value) = HeaderValue::from_str(entity_tag)
} else { {
"public, max-age=3600, must-revalidate" headers.insert(header::ETAG, value);
}
}
fn local_asset_entity_tag(metadata: &std::fs::Metadata) -> Option<String> {
let modified = metadata.modified().ok()?;
let nanos = modified
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos();
Some(format!("\"{nanos:x}-{:x}\"", metadata.len()))
}
fn if_none_match_matches(headers: &HeaderMap, entity_tag: &str) -> bool {
let Some(header_value) = headers
.get(header::IF_NONE_MATCH)
.and_then(|value| value.to_str().ok())
else {
return false;
}; };
response.headers_mut().insert( header_value.split(',').any(|candidate| {
let candidate = candidate.trim();
candidate == "*" || candidate.trim_start_matches("W/") == entity_tag
})
}
fn set_proxied_cache_control(headers: &mut HeaderMap, path: &str, status: StatusCode) {
if status.is_success() || status == StatusCode::NOT_MODIFIED {
headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_static(asset_cache_control(path)),
);
return;
}
headers.insert(
header::CACHE_CONTROL, header::CACHE_CONTROL,
HeaderValue::from_static(cache_control), HeaderValue::from_static(UPSTREAM_FAILURE_CACHE_CONTROL),
); );
response for name in UPSTREAM_FAILURE_STRIPPED_HEADERS {
headers.remove(*name);
}
} }
fn set_font_cors(headers: &mut HeaderMap) { fn set_font_cors(headers: &mut HeaderMap) {
@@ -210,7 +284,411 @@ fn set_known_asset_content_type(headers: &mut HeaderMap, path: &str) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::spa_static::{
LONG_LIVED_ASSET_CACHE_CONTROL, REVALIDATED_ASSET_CACHE_CONTROL, is_hashed_asset,
};
use super::*; use super::*;
use crate::config::AppProxyConfig;
use crate::discovery_cache::DiscoveryCache;
use axum::Router;
use axum::http::Request as HttpRequest;
use axum::http::header::HeaderName;
use fluxer_common::config::GeoipSourceConfig;
use fluxer_common::geoip::{GeoipConfig, GeoipResolver};
use std::sync::Arc;
async fn spawn_upstream(status: StatusCode, cache_control: &'static str) -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let router = Router::new().fallback(move || async move {
let mut response = Response::new(Body::from("upstream-bytes"));
*response.status_mut() = status;
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static(cache_control),
);
response
});
tokio::spawn(async move {
axum::serve(listener, router).await.unwrap();
});
format!("http://{addr}")
}
fn upstream_backed_state(cdn_endpoint: &str) -> AppState {
let mut config = AppProxyConfig::from_env();
config.static_cdn_endpoint = Some(cdn_endpoint.to_owned());
AppState {
config: Arc::new(config),
http_client: reqwest::Client::new(),
discovery_cache: Arc::new(DiscoveryCache::new()),
geoip: Arc::new(GeoipResolver::from_config(&GeoipConfig {
geoip_source: GeoipSourceConfig::Filesystem {
maxmind_db_path: None,
},
geoip_s3_config: None,
trust_client_ip_header: false,
client_ip_header_name: "x-forwarded-for".to_owned(),
})),
invite_meta: None,
index_html: None,
}
}
async fn proxied_asset(
status: StatusCode,
upstream_cache_control: &'static str,
asset_path: &str,
) -> Response {
let endpoint = spawn_upstream(status, upstream_cache_control).await;
let state = upstream_backed_state(&endpoint);
let request = HttpRequest::builder()
.uri(format!("/assets/{asset_path}"))
.body(Body::empty())
.unwrap();
proxy_assets(State(state), Path(asset_path.to_owned()), request).await
}
fn cache_control_of(response: &Response) -> Option<&str> {
response
.headers()
.get(header::CACHE_CONTROL)
.and_then(|value| value.to_str().ok())
}
#[tokio::test]
async fn a_proxied_asset_overrides_a_shorter_upstream_lifetime() {
let response = proxied_asset(
StatusCode::OK,
"public, max-age=3600, must-revalidate",
"2d715e4730758083.worker.js",
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
cache_control_of(&response),
Some(LONG_LIVED_ASSET_CACHE_CONTROL)
);
}
#[tokio::test]
async fn an_asset_without_a_content_hash_is_never_promised_to_never_change() {
let response = proxied_asset(
StatusCode::OK,
"public, max-age=31536000, immutable",
"voice_engine_bg.wasm",
)
.await;
assert_eq!(
cache_control_of(&response),
Some(REVALIDATED_ASSET_CACHE_CONTROL),
"a stable filename can be redeployed over, so it must stay revalidatable"
);
}
#[tokio::test]
async fn revalidated_hashed_asset_keeps_our_lifetime_on_not_modified() {
let response = proxied_asset(
StatusCode::NOT_MODIFIED,
"public, max-age=60",
"2d715e4730758083.worker.js",
)
.await;
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
assert_eq!(
cache_control_of(&response),
Some(LONG_LIVED_ASSET_CACHE_CONTROL)
);
}
#[tokio::test]
async fn an_asset_without_a_content_hash_keeps_our_policy_on_not_modified() {
let response = proxied_asset(
StatusCode::NOT_MODIFIED,
"public, max-age=31536000, immutable",
"voice_engine_bg.wasm",
)
.await;
assert_eq!(
cache_control_of(&response),
Some(REVALIDATED_ASSET_CACHE_CONTROL)
);
}
#[tokio::test]
async fn upstream_failure_is_never_stamped_with_an_asset_lifetime() {
let response = proxied_asset(
StatusCode::NOT_FOUND,
"no-store",
"2d715e4730758083.worker.js",
)
.await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
assert_eq!(cache_control_of(&response), Some("no-store"));
}
#[tokio::test]
async fn a_not_found_carrying_a_long_upstream_lifetime_is_rewritten_to_no_store() {
let response = proxied_asset(
StatusCode::NOT_FOUND,
"public, max-age=31536000, immutable",
"2d715e4730758083.worker.js",
)
.await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
assert_eq!(
cache_control_of(&response),
Some(UPSTREAM_FAILURE_CACHE_CONTROL),
"a cdn or bucket error page with its own year would pin the miss for a year"
);
}
#[tokio::test]
async fn a_bad_gateway_carrying_a_long_upstream_lifetime_is_rewritten_to_no_store() {
let response = proxied_asset(
StatusCode::BAD_GATEWAY,
"public, max-age=604800",
"2d715e4730758083.worker.js",
)
.await;
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
assert_eq!(
cache_control_of(&response),
Some(UPSTREAM_FAILURE_CACHE_CONTROL)
);
}
#[tokio::test]
async fn a_server_error_carrying_a_long_upstream_lifetime_is_rewritten_to_no_store() {
let response = proxied_asset(
StatusCode::INTERNAL_SERVER_ERROR,
"public, max-age=86400, immutable",
"voice_engine_bg.wasm",
)
.await;
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(
cache_control_of(&response),
Some(UPSTREAM_FAILURE_CACHE_CONTROL)
);
}
#[test]
fn a_failure_drops_the_cdn_lifetimes_a_success_keeps() {
let long_lived = || {
let mut headers = HeaderMap::new();
headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=31536000, immutable"),
);
headers.insert(
HeaderName::from_static("cdn-cache-control"),
HeaderValue::from_static("public, max-age=31536000"),
);
headers.insert(
header::EXPIRES,
HeaderValue::from_static("Thu, 31 Dec 2099 23:59:59 GMT"),
);
headers
};
let mut ok = long_lived();
set_proxied_cache_control(&mut ok, "2d715e4730758083.worker.js", StatusCode::OK);
assert!(
ok.contains_key("cdn-cache-control"),
"positive control: a real asset still reaches the cdn with its own long lifetime"
);
assert!(ok.contains_key(header::EXPIRES));
let mut failed = long_lived();
set_proxied_cache_control(
&mut failed,
"2d715e4730758083.worker.js",
StatusCode::NOT_FOUND,
);
assert_eq!(
failed
.get(header::CACHE_CONTROL)
.and_then(|value| value.to_str().ok()),
Some(UPSTREAM_FAILURE_CACHE_CONTROL)
);
assert!(
!failed.contains_key("cdn-cache-control"),
"a cdn honours cdn-cache-control over cache-control, so the error would still be pinned"
);
assert!(!failed.contains_key(header::EXPIRES));
}
struct LocalAssetDir {
root: std::path::PathBuf,
}
impl LocalAssetDir {
fn with_asset(name: &str, bytes: &[u8]) -> Self {
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!("fluxer-local-asset-{unique}-{name}"));
std::fs::create_dir_all(root.join("assets")).unwrap();
std::fs::write(root.join("assets").join(name), bytes).unwrap();
Self { root }
}
fn dir(&self) -> &str {
self.root.to_str().unwrap()
}
}
impl Drop for LocalAssetDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
fn entity_tag_of(response: &Response) -> Option<String> {
response
.headers()
.get(header::ETAG)
.and_then(|value| value.to_str().ok())
.map(ToOwned::to_owned)
}
fn cors_origin_of(response: &Response) -> Option<&str> {
response
.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.and_then(|value| value.to_str().ok())
}
#[tokio::test]
async fn local_font_revalidation_keeps_cross_origin_access() {
let fixture = LocalAssetDir::with_asset("0018072843a46dc4.woff2", b"wOF2stub");
let first = serve_local_asset(
fixture.dir(),
"assets/0018072843a46dc4.woff2",
&HeaderMap::new(),
)
.await;
assert_eq!(cors_origin_of(&first), Some(CORS_ALLOW_ANY_VALUE));
let entity_tag = entity_tag_of(&first).expect("first response carries a validator");
let mut conditional = HeaderMap::new();
conditional.insert(
header::IF_NONE_MATCH,
HeaderValue::from_str(&entity_tag).unwrap(),
);
let second =
serve_local_asset(fixture.dir(), "assets/0018072843a46dc4.woff2", &conditional).await;
assert_eq!(second.status(), StatusCode::NOT_MODIFIED);
assert_eq!(
cors_origin_of(&second),
Some(CORS_ALLOW_ANY_VALUE),
"a 304 without the CORS header fails the cross-origin font fetch the 200 allowed"
);
}
#[tokio::test]
async fn local_hashed_asset_is_served_with_an_entity_tag() {
let fixture = LocalAssetDir::with_asset("356aaade04a117b1.js", b"console.log(1)");
let response = serve_local_asset(
fixture.dir(),
"assets/356aaade04a117b1.js",
&HeaderMap::new(),
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
cache_control_of(&response),
Some(LONG_LIVED_ASSET_CACHE_CONTROL)
);
assert!(
entity_tag_of(&response).is_some(),
"a year-long asset with no validator forces a full re-download on any revalidation"
);
}
#[tokio::test]
async fn local_asset_revalidation_returns_not_modified() {
let fixture = LocalAssetDir::with_asset("f00dcafe12345678.css", b"body{}");
let first = serve_local_asset(
fixture.dir(),
"assets/f00dcafe12345678.css",
&HeaderMap::new(),
)
.await;
let entity_tag = entity_tag_of(&first).expect("first response carries a validator");
let mut conditional = HeaderMap::new();
conditional.insert(
header::IF_NONE_MATCH,
HeaderValue::from_str(&entity_tag).unwrap(),
);
let second =
serve_local_asset(fixture.dir(), "assets/f00dcafe12345678.css", &conditional).await;
assert_eq!(second.status(), StatusCode::NOT_MODIFIED);
assert_eq!(entity_tag_of(&second).as_deref(), Some(entity_tag.as_str()));
assert_eq!(
cache_control_of(&second),
Some(LONG_LIVED_ASSET_CACHE_CONTROL)
);
}
#[tokio::test]
async fn local_asset_with_a_stale_entity_tag_is_resent_in_full() {
let fixture = LocalAssetDir::with_asset("voice_engine_bg.wasm", b"\0asm");
let mut conditional = HeaderMap::new();
conditional.insert(
header::IF_NONE_MATCH,
HeaderValue::from_static("\"stale-from-a-previous-build\""),
);
let response =
serve_local_asset(fixture.dir(), "assets/voice_engine_bg.wasm", &conditional).await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
cache_control_of(&response),
Some(REVALIDATED_ASSET_CACHE_CONTROL)
);
assert!(
entity_tag_of(&response).is_some(),
"a year-long asset with no validator forces a full re-download on any revalidation"
);
}
#[tokio::test]
async fn a_local_content_hashed_asset_is_promised_to_never_change() {
let fixture = LocalAssetDir::with_asset("2d715e4730758083.worker.js", b"self.onmessage=0");
let response = serve_local_asset(
fixture.dir(),
"assets/2d715e4730758083.worker.js",
&HeaderMap::new(),
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
cache_control_of(&response),
Some(LONG_LIVED_ASSET_CACHE_CONTROL)
);
assert!(is_hashed_asset("assets/2d715e4730758083.worker.js"));
}
#[test] #[test]
fn known_js_asset_overrides_upstream_octet_stream() { fn known_js_asset_overrides_upstream_octet_stream() {
+716 -26
View File
@@ -19,7 +19,7 @@ use axum::{
use std::path::Path; use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use super::spa_static::{CORS_ALLOW_ANY_VALUE, guess_mime, is_font_mime, is_hashed_asset}; use super::spa_static::{CORS_ALLOW_ANY_VALUE, guess_mime, is_font_mime};
const ACCEPT_CH_VALUE: &str = "DPR, Sec-CH-DPR, Sec-CH-Width, Save-Data, ECT, Downlink"; const ACCEPT_CH_VALUE: &str = "DPR, Sec-CH-DPR, Sec-CH-Width, Save-Data, ECT, Downlink";
const CRITICAL_CH_VALUE: &str = "Sec-CH-DPR, Sec-CH-Width, Save-Data"; const CRITICAL_CH_VALUE: &str = "Sec-CH-DPR, Sec-CH-Width, Save-Data";
@@ -32,22 +32,29 @@ pub async fn spa_catch_all(
) -> Response { ) -> Response {
let request_path = request.uri().path(); let request_path = request.uri().path();
if is_static_root_file(request_path) { if let Some(cache_control) = static_root_file_cache_control(request_path) {
return serve_static_file(&state.config.static_dir, request_path).await; return serve_static_file(&state.config.static_dir, request_path, cache_control).await;
} }
serve_spa_index(&state, &headers, request_path).await serve_spa_index(&state, &headers, request_path).await
} }
const STATIC_ROOT_FILES: &[&str] = &["/robots.txt"]; const CRAWL_CONTROL_CACHE_CONTROL: &str = "public, max-age=300, must-revalidate";
fn is_static_root_file(request_path: &str) -> bool { const STATIC_ROOT_FILES: &[(&str, &str)] = &[("/robots.txt", CRAWL_CONTROL_CACHE_CONTROL)];
fn static_root_file_cache_control(request_path: &str) -> Option<&'static str> {
STATIC_ROOT_FILES STATIC_ROOT_FILES
.iter() .iter()
.any(|candidate| request_path.eq_ignore_ascii_case(candidate)) .find(|(candidate, _)| request_path.eq_ignore_ascii_case(candidate))
.map(|(_, cache_control)| *cache_control)
} }
async fn serve_static_file(static_dir: &str, request_path: &str) -> Response { async fn serve_static_file(
static_dir: &str,
request_path: &str,
cache_control: &'static str,
) -> Response {
let file_path = Path::new(static_dir).join(request_path.trim_start_matches('/')); let file_path = Path::new(static_dir).join(request_path.trim_start_matches('/'));
let resolved = match tokio::fs::canonicalize(&file_path).await { let resolved = match tokio::fs::canonicalize(&file_path).await {
@@ -75,11 +82,6 @@ async fn serve_static_file(static_dir: &str, request_path: &str) -> Response {
}; };
let mime_type = guess_mime(request_path); let mime_type = guess_mime(request_path);
let cache_control = if is_hashed_asset(request_path) {
"public, max-age=31536000, immutable"
} else {
"public, max-age=3600, must-revalidate"
};
let mut response = content.into_response(); let mut response = content.into_response();
if let Ok(ct) = HeaderValue::from_str(mime_type) { if let Ok(ct) = HeaderValue::from_str(mime_type) {
@@ -118,19 +120,23 @@ async fn serve_spa_index(state: &AppState, headers: &HeaderMap, request_path: &s
.static_cdn_endpoint .static_cdn_endpoint
.as_deref() .as_deref()
.unwrap_or(""); .unwrap_or("");
let media_endpoint = runtime_csp_sources.media_endpoint.as_deref().unwrap_or("");
let csp = build_csp(&state.config.csp, &nonce, &runtime_csp_sources); let csp = build_csp(&state.config.csp, &nonce, &runtime_csp_sources);
let geoip = build_geoip_response(state.geoip.lookup(headers)); let geoip = build_geoip_response(state.geoip.lookup(headers));
let script_tag = build_bootstrap_script(&state.config, &discovery, &geoip, &nonce); let script_tag = build_bootstrap_script(&state.config, &discovery, &geoip, &nonce);
if let Some(snapshot) = should_serve_frozen(&time_freeze) { if let Some(snapshot) = should_serve_frozen(&time_freeze) {
let frozen_html = String::from_utf8_lossy(&snapshot.index_html); let frozen_html = String::from_utf8_lossy(&snapshot.index_html);
let mut html = inject_bootstrap(&frozen_html, &nonce, &script_tag, static_cdn_endpoint); let dev_buster = should_bust_dev_assets.then(current_dev_asset_cache_buster);
if let Some(meta) = &invite_meta { let html = render_spa_document(
html = inject_invite_meta(&html, meta); &frozen_html,
} &nonce,
if should_bust_dev_assets { &script_tag,
html = append_dev_asset_cache_buster(&html, &current_dev_asset_cache_buster()); static_cdn_endpoint,
} media_endpoint,
invite_meta.as_ref(),
dev_buster.as_deref(),
);
return build_spa_response(html, &csp, debug_header.as_deref(), should_bust_dev_assets); return build_spa_response(html, &csp, debug_header.as_deref(), should_bust_dev_assets);
} }
@@ -139,16 +145,39 @@ async fn serve_spa_index(state: &AppState, headers: &HeaderMap, request_path: &s
Err(response) => return response, Err(response) => return response,
}; };
let mut html = inject_bootstrap(&raw_html, &nonce, &script_tag, static_cdn_endpoint); let dev_buster = should_bust_dev_assets.then(current_dev_asset_cache_buster);
if let Some(meta) = &invite_meta { let html = render_spa_document(
html = inject_invite_meta(&html, meta); &raw_html,
} &nonce,
if should_bust_dev_assets { &script_tag,
html = append_dev_asset_cache_buster(&html, &current_dev_asset_cache_buster()); static_cdn_endpoint,
} media_endpoint,
invite_meta.as_ref(),
dev_buster.as_deref(),
);
build_spa_response(html, &csp, debug_header.as_deref(), should_bust_dev_assets) build_spa_response(html, &csp, debug_header.as_deref(), should_bust_dev_assets)
} }
fn render_spa_document(
html: &str,
nonce: &str,
script_tag: &str,
static_cdn_endpoint: &str,
media_endpoint: &str,
invite_meta: Option<&InvitePageMeta>,
dev_asset_cache_buster: Option<&str>,
) -> String {
let mut document =
inject_bootstrap(html, nonce, script_tag, static_cdn_endpoint, media_endpoint);
if let Some(meta) = invite_meta {
document = inject_invite_meta(&document, meta);
}
if let Some(buster) = dev_asset_cache_buster {
document = append_dev_asset_cache_buster(&document, buster);
}
document
}
async fn refresh_discovery_for_spa(state: &AppState) -> Option<DiscoveryResponse> { async fn refresh_discovery_for_spa(state: &AppState) -> Option<DiscoveryResponse> {
state state
.discovery_cache .discovery_cache
@@ -466,8 +495,21 @@ fn append_cache_buster_query(value: &str, buster: &str) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::spa_static::LONG_LIVED_ASSET_CACHE_CONTROL;
use super::*; use super::*;
fn is_static_root_file(request_path: &str) -> bool {
static_root_file_cache_control(request_path).is_some()
}
use crate::config::{AppProxyConfig, ReleaseChannel};
use crate::discovery_cache::DiscoveryCache;
use axum::Router;
use axum::body::Body;
use fluxer_common::config::GeoipSourceConfig;
use fluxer_common::geoip::{GeoipConfig, GeoipResolver};
use std::sync::Arc;
#[test] #[test]
fn dev_asset_cache_buster_rewrites_script_and_link_assets() { fn dev_asset_cache_buster_rewrites_script_and_link_assets() {
let html = r#"<link rel="preconnect" href="https://example.test"><link href="/assets/main.css?abcdef1234567890"><script src="https://example.test/assets/main.abcdef1234567890.js"></script><script src="/assets/unversioned.js"></script><link rel="manifest" href="/manifest.json">"#; let html = r#"<link rel="preconnect" href="https://example.test"><link href="/assets/main.css?abcdef1234567890"><script src="https://example.test/assets/main.abcdef1234567890.js"></script><script src="/assets/unversioned.js"></script><link rel="manifest" href="/manifest.json">"#;
@@ -516,6 +558,654 @@ mod tests {
assert!(!is_static_root_file("/users/1.2.3")); assert!(!is_static_root_file("/users/1.2.3"));
} }
const SHELL_WITH_A_NONCE_HOLE: &str = r#"<!doctype html><html><head><title>Fluxer</title><script nonce="{{CSP_NONCE_PLACEHOLDER}}"></script><script src="/assets/app.js"></script></head><body></body></html>"#;
fn sample_invite_meta() -> InvitePageMeta {
InvitePageMeta {
title: "Join Sample Space".to_owned(),
description: "A sample invite".to_owned(),
image_url: None,
}
}
#[test]
fn the_rendered_document_always_carries_the_bootstrap_and_a_real_nonce() {
let rendered = render_spa_document(
SHELL_WITH_A_NONCE_HOLE,
"reqnonce",
"<script>booted</script>",
"https://static.example.test",
"",
None,
None,
);
assert!(!rendered.contains("{{CSP_NONCE_PLACEHOLDER}}"));
assert!(rendered.contains(r#"nonce="reqnonce""#));
assert!(rendered.contains("<script>booted</script>"));
}
#[test]
fn invite_metadata_reaches_the_rendered_document_only_when_resolved() {
let meta = sample_invite_meta();
let with_meta = render_spa_document(
SHELL_WITH_A_NONCE_HOLE,
"reqnonce",
"<script>booted</script>",
"",
"",
Some(&meta),
None,
);
let without_meta = render_spa_document(
SHELL_WITH_A_NONCE_HOLE,
"reqnonce",
"<script>booted</script>",
"",
"",
None,
None,
);
assert!(with_meta.contains("Join Sample Space"));
assert!(with_meta.contains("og:title"));
assert!(!without_meta.contains("Join Sample Space"));
assert!(!without_meta.contains("og:title"));
}
#[test]
fn the_dev_cache_buster_reaches_the_rendered_document_only_when_supplied() {
let busted = render_spa_document(
SHELL_WITH_A_NONCE_HOLE,
"reqnonce",
"<script>booted</script>",
"",
"",
None,
Some("9911"),
);
let untouched = render_spa_document(
SHELL_WITH_A_NONCE_HOLE,
"reqnonce",
"<script>booted</script>",
"",
"",
None,
None,
);
assert!(busted.contains(r#"src="/assets/app.js?_=9911""#));
assert!(untouched.contains(r#"src="/assets/app.js""#));
assert!(!untouched.contains("_=9911"));
}
#[cfg(feature = "time-freeze")]
#[test]
fn the_frozen_shell_is_never_served_with_an_unfilled_nonce_or_a_missing_bootstrap() {
let frozen = String::from_utf8_lossy(&crate::frozen_snapshots::STABLE_SNAPSHOT.index_html)
.into_owned();
assert!(
frozen.contains("{{CSP_NONCE_PLACEHOLDER}}"),
"the captured shell should still carry the hole this test proves we fill"
);
let served = render_spa_document(
&frozen,
"frozennonce",
"<script>frozenboot</script>",
"https://fluxerstatic.com",
"",
None,
None,
);
assert!(
!served.contains("{{CSP_NONCE_PLACEHOLDER}}"),
"a frozen-served shell shipped a literal nonce placeholder"
);
assert!(
served.contains(r#"nonce="frozennonce""#),
"a frozen-served shell lost its per-request nonce"
);
assert!(
served.contains("<script>frozenboot</script>"),
"a frozen-served shell shipped without the bootstrap script"
);
}
const SHELL_WITH_ENDPOINT_HOLES: &str = r#"<!doctype html><html><head><title>Fluxer</title><link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}">
<link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}" crossorigin>
<link rel="preconnect" href="{{MEDIA_ENDPOINT}}">
<link rel="icon" type="image/png" sizes="32x32" href="{{STATIC_CDN_ENDPOINT}}/web/favicon-32x32.png"><link rel="apple-touch-icon" sizes="180x180" href="{{STATIC_CDN_ENDPOINT}}/web/apple-touch-icon.png"><script nonce="{{CSP_NONCE_PLACEHOLDER}}"></script><script src="/assets/app.js"></script></head><body></body></html>"#;
#[test]
fn the_static_cdn_argument_resolves_every_hole_the_shell_carries() {
let rendered = render_spa_document(
SHELL_WITH_ENDPOINT_HOLES,
"reqnonce",
"<script>booted</script>",
"https://cdn.example.test/",
"https://media.example.test",
None,
None,
);
assert!(
rendered
.contains(r#"<link rel="preconnect" href="https://cdn.example.test" crossorigin>"#),
"the static CDN argument never reached the anonymous preconnect"
);
assert!(
rendered.contains(r#"<link rel="preconnect" href="https://cdn.example.test">"#),
"the static CDN argument never reached the credentialed preconnect"
);
assert!(
rendered.contains(r#"href="https://cdn.example.test/web/favicon-32x32.png""#),
"the favicon href was not resolved against the static CDN argument"
);
assert!(
rendered.contains(r#"href="https://cdn.example.test/web/apple-touch-icon.png""#),
"the touch-icon href was not resolved against the static CDN argument"
);
assert!(!rendered.contains("{{STATIC_CDN_ENDPOINT}}"));
assert_eq!(rendered.matches("preconnect").count(), 3);
}
#[test]
fn the_media_argument_is_resolved_and_weighed_against_the_static_cdn() {
let distinct = render_spa_document(
SHELL_WITH_ENDPOINT_HOLES,
"reqnonce",
"<script>booted</script>",
"https://cdn.example.test",
"https://media.example.test/",
None,
None,
);
assert!(
distinct.contains(r#"<link rel="preconnect" href="https://media.example.test">"#),
"the media argument never reached the media preconnect"
);
assert!(!distinct.contains("{{MEDIA_ENDPOINT}}"));
assert_eq!(distinct.matches("preconnect").count(), 3);
let shared = render_spa_document(
SHELL_WITH_ENDPOINT_HOLES,
"reqnonce",
"<script>booted</script>",
"https://cdn.example.test",
"https://cdn.example.test",
None,
None,
);
assert!(
shared.contains(r#"<link rel="preconnect" href="https://cdn.example.test">"#),
"the static preconnects must survive a media endpoint that collapses onto them"
);
assert_eq!(
shared.matches("preconnect").count(),
2,
"a media endpoint equal to the static CDN must not warm a third socket"
);
}
const DISCOVERY_BODY_WITH_BOTH_ENDPOINTS: &str = r#"{"api_code_version":"proxy-test","endpoints":{"static_cdn":"https://cdn.example.test","media":"https://media.example.test"}}"#;
const DISCOVERY_BODY_WITHOUT_ENDPOINTS: &str = r#"{"api_code_version":"proxy-test"}"#;
async fn spawn_local_origin(payload: &'static str, content_type: &'static str) -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let router = Router::new().fallback(move || async move {
let mut response = Response::new(Body::from(payload));
response
.headers_mut()
.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
response
});
tokio::spawn(async move {
axum::serve(listener, router).await.unwrap();
});
format!("http://{addr}/")
}
async fn spa_state_serving(channel: ReleaseChannel, cached_shell: Option<&str>) -> AppState {
assemble_spa_state(
channel,
cached_shell,
DISCOVERY_BODY_WITH_BOTH_ENDPOINTS,
None,
None,
)
.await
}
async fn spa_state_reading_its_shell_from(index_upstream_url: String) -> AppState {
assemble_spa_state(
ReleaseChannel::Stable,
None,
DISCOVERY_BODY_WITH_BOTH_ENDPOINTS,
None,
Some(index_upstream_url),
)
.await
}
async fn spa_state_without_discovered_endpoints(static_cdn_fallback: Option<&str>) -> AppState {
assemble_spa_state(
ReleaseChannel::Canary,
Some(SHELL_WITH_ENDPOINT_HOLES),
DISCOVERY_BODY_WITHOUT_ENDPOINTS,
static_cdn_fallback,
None,
)
.await
}
async fn assemble_spa_state(
channel: ReleaseChannel,
cached_shell: Option<&str>,
discovery_body: &'static str,
static_cdn_fallback: Option<&str>,
index_upstream_url: Option<String>,
) -> AppState {
let discovery_upstream_url = spawn_local_origin(discovery_body, "application/json").await;
let mut config = AppProxyConfig::from_env();
config.release_channel = channel;
config.time_freeze_enabled = true;
config.index_upstream_url = index_upstream_url;
config.static_cdn_endpoint = static_cdn_fallback.map(ToOwned::to_owned);
config.trust_client_ip_header = false;
config.discovery_upstream_url = discovery_upstream_url;
AppState {
config: Arc::new(config),
http_client: reqwest::Client::new(),
discovery_cache: Arc::new(DiscoveryCache::new()),
geoip: Arc::new(GeoipResolver::from_config(&GeoipConfig {
geoip_source: GeoipSourceConfig::Filesystem {
maxmind_db_path: None,
},
geoip_s3_config: None,
trust_client_ip_header: false,
client_ip_header_name: "x-forwarded-for".to_owned(),
})),
invite_meta: None,
index_html: cached_shell.map(Arc::from),
}
}
fn nonce_granted_by(response: &Response) -> String {
let policy = response
.headers()
.get(header::CONTENT_SECURITY_POLICY)
.expect("the document was served without a content security policy")
.to_str()
.unwrap();
let opening = policy
.find("'nonce-")
.expect("the content security policy granted no nonce at all");
let remainder = &policy[opening + "'nonce-".len()..];
let closing = remainder
.find('\'')
.expect("the content security policy left its nonce source unterminated");
remainder[..closing].to_owned()
}
async fn read_document(response: Response) -> String {
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[tokio::test]
async fn the_live_branch_serves_a_rendered_document_and_not_the_raw_shell() {
let state =
spa_state_serving(ReleaseChannel::Canary, Some(SHELL_WITH_ENDPOINT_HOLES)).await;
let response = serve_spa_index(&state, &HeaderMap::new(), "/channels/@me").await;
assert_eq!(response.status(), StatusCode::OK);
let granted_nonce = nonce_granted_by(&response);
let served = read_document(response).await;
assert!(
!served.contains("{{CSP_NONCE_PLACEHOLDER}}"),
"the live branch shipped an unfilled nonce hole"
);
assert!(
served.contains(&format!(
r#"<script nonce="{granted_nonce}">window.__FLUXER_BOOTSTRAP__"#
)),
"the live bootstrap was not granted the nonce its own policy header carries"
);
assert_eq!(
served
.matches(&format!(r#"nonce="{granted_nonce}""#))
.count(),
served.matches(r#"nonce=""#).count(),
"the live document carries a nonce its own policy header never granted"
);
assert!(
!served.contains("{{STATIC_CDN_ENDPOINT}}"),
"the live branch shipped an unresolved static CDN hole"
);
assert!(
!served.contains("{{MEDIA_ENDPOINT}}"),
"the live branch shipped an unresolved media hole"
);
assert!(
served.contains("window.__FLUXER_BOOTSTRAP__"),
"the live branch shipped without a bootstrap script"
);
assert!(
served
.contains(r#"<link rel="preconnect" href="https://cdn.example.test" crossorigin>"#),
"the discovered static CDN never reached the served document"
);
assert!(
served.contains(r#"<link rel="preconnect" href="https://media.example.test">"#),
"the discovered media endpoint never reached the served document"
);
}
#[cfg(feature = "time-freeze")]
#[tokio::test]
async fn the_frozen_document_is_served_with_its_asset_urls_untouched() {
let captured =
String::from_utf8_lossy(&crate::frozen_snapshots::STABLE_SNAPSHOT.index_html)
.into_owned();
assert!(
captured.contains(
r#"<link rel="stylesheet" href="https://fluxerstatic.com/fonts/ibm-plex.css">"#
),
"the captured shell no longer carries the asset URL this test proves we leave alone"
);
let state = spa_state_serving(ReleaseChannel::Stable, None).await;
let response = serve_spa_index(&state, &HeaderMap::new(), "/channels/@me").await;
assert_eq!(response.status(), StatusCode::OK);
let served = read_document(response).await;
assert!(
served.contains(
r#"<link rel="stylesheet" href="https://fluxerstatic.com/fonts/ibm-plex.css">"#
),
"the frozen document's stylesheet URL was rewritten on the primary hosted path"
);
assert!(
served.contains(r#"<link rel="manifest" href="/manifest.json">"#),
"the frozen document's manifest URL was rewritten on the primary hosted path"
);
assert!(
!served.contains("?_=") && !served.contains("&_="),
"the frozen document was served with a development cache-busting query"
);
}
#[cfg(feature = "time-freeze")]
#[tokio::test]
async fn the_frozen_branch_serves_a_rendered_document_and_not_the_raw_shell() {
let captured =
String::from_utf8_lossy(&crate::frozen_snapshots::STABLE_SNAPSHOT.index_html)
.into_owned();
assert!(
captured.contains("{{CSP_NONCE_PLACEHOLDER}}"),
"the captured shell no longer carries the nonce hole this test proves we fill"
);
assert!(
!captured.contains("window.__FLUXER_BOOTSTRAP__"),
"the captured shell already carries a bootstrap, so this test can no longer tell a rendered document from a raw shell"
);
let state = spa_state_serving(ReleaseChannel::Stable, None).await;
let response = serve_spa_index(&state, &HeaderMap::new(), "/channels/@me").await;
assert_eq!(response.status(), StatusCode::OK);
let granted_nonce = nonce_granted_by(&response);
let served = read_document(response).await;
assert!(
!served.contains("{{CSP_NONCE_PLACEHOLDER}}"),
"the primary hosted path shipped a literal nonce placeholder"
);
assert_eq!(
served
.matches(&format!(r#"nonce="{granted_nonce}""#))
.count(),
served.matches(r#"nonce=""#).count(),
"the frozen document carries a nonce its own policy header never granted"
);
assert!(
served.contains(&format!(
r#"<script nonce="{granted_nonce}">window.__FLUXER_BOOTSTRAP__"#
)),
"the primary hosted path shipped without a bootstrap script the browser will run"
);
let second = serve_spa_index(&state, &HeaderMap::new(), "/channels/@me").await;
assert_ne!(
nonce_granted_by(&second),
granted_nonce,
"two frozen requests were granted the same nonce"
);
}
#[cfg(feature = "time-freeze")]
#[tokio::test]
async fn every_branch_announces_which_snapshot_decision_it_took() {
let frozen_state = spa_state_serving(ReleaseChannel::Stable, None).await;
let frozen = serve_spa_index(&frozen_state, &HeaderMap::new(), "/channels/@me").await;
assert_eq!(
frozen
.headers()
.get("x-time-freeze")
.expect("the frozen response announced no snapshot decision")
.to_str()
.unwrap(),
format!(
"frozen; sha={}",
crate::frozen_snapshots::STABLE_SNAPSHOT.sha
),
"the frozen response named the wrong snapshot"
);
let live_state =
spa_state_serving(ReleaseChannel::Canary, Some(SHELL_WITH_ENDPOINT_HOLES)).await;
let live = serve_spa_index(&live_state, &HeaderMap::new(), "/channels/@me").await;
assert_eq!(
live.headers()
.get("x-time-freeze")
.expect("the live response announced no snapshot decision")
.to_str()
.unwrap(),
"no-snapshot",
"a channel with no snapshot claimed one anyway"
);
}
#[cfg(feature = "time-freeze")]
#[tokio::test]
async fn the_frozen_shell_is_never_served_with_the_asset_lifetime() {
let state = spa_state_serving(ReleaseChannel::Stable, None).await;
let response = serve_spa_index(&state, &HeaderMap::new(), "/channels/@me").await;
assert_eq!(
response
.headers()
.get("x-time-freeze")
.expect("the response announced no snapshot decision")
.to_str()
.unwrap(),
format!(
"frozen; sha={}",
crate::frozen_snapshots::STABLE_SNAPSHOT.sha
),
"this test never reached the frozen branch, so it proves nothing about it"
);
let cache_control = response
.headers()
.get(header::CACHE_CONTROL)
.expect("the frozen shell was served without a cache policy at all")
.to_str()
.unwrap();
assert_eq!(
cache_control, "no-cache",
"the frozen document naming the hashed bundle must be revalidated on every load"
);
assert_ne!(
cache_control, LONG_LIVED_ASSET_CACHE_CONTROL,
"a frozen shell cached for a year pins every returning visitor to the deployed-over bundle"
);
}
#[tokio::test]
async fn a_crawl_control_document_is_never_served_with_the_asset_lifetime() {
let root = std::env::temp_dir().join(format!(
"fluxer-app-proxy-crawl-control-{}",
std::process::id()
));
tokio::fs::create_dir_all(&root).await.unwrap();
tokio::fs::write(root.join("robots.txt"), "User-agent: *\nDisallow:\n")
.await
.unwrap();
let static_dir = root.to_str().unwrap();
let policy = static_root_file_cache_control("/robots.txt")
.expect("robots.txt is no longer served as a static root file");
assert!(
static_root_file_cache_control("/channels/@me").is_none(),
"an application route was mistaken for a static root file"
);
let response = serve_static_file(static_dir, "/robots.txt", policy).await;
assert_eq!(response.status(), StatusCode::OK);
let cache_control = response
.headers()
.get(header::CACHE_CONTROL)
.expect("the crawl-control document was served without a cache policy at all")
.to_str()
.unwrap();
assert_eq!(cache_control, CRAWL_CONTROL_CACHE_CONTROL);
assert_ne!(
cache_control, LONG_LIVED_ASSET_CACHE_CONTROL,
"a crawl rule change cannot reach a crawler that already fetched a year-long robots.txt"
);
tokio::fs::remove_dir_all(&root).await.unwrap();
}
#[tokio::test]
async fn the_shell_is_never_served_with_the_asset_lifetime() {
let state =
spa_state_serving(ReleaseChannel::Canary, Some(SHELL_WITH_ENDPOINT_HOLES)).await;
let response = serve_spa_index(&state, &HeaderMap::new(), "/channels/@me").await;
let cache_control = response
.headers()
.get(header::CACHE_CONTROL)
.expect("the shell was served without a cache policy at all")
.to_str()
.unwrap();
assert_eq!(
cache_control, "no-cache",
"the document naming the hashed bundle must be revalidated on every load"
);
assert_ne!(
cache_control, LONG_LIVED_ASSET_CACHE_CONTROL,
"a shell cached for a year pins every returning visitor to the deployed-over bundle"
);
}
#[tokio::test]
async fn an_index_upstream_replaces_the_snapshot_with_an_unstorable_busted_document() {
let index_upstream_url = spawn_local_origin(SHELL_WITH_ENDPOINT_HOLES, "text/html").await;
let state = spa_state_reading_its_shell_from(index_upstream_url).await;
let response = serve_spa_index(&state, &HeaderMap::new(), "/channels/@me").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(header::CACHE_CONTROL)
.unwrap()
.to_str()
.unwrap(),
DEV_NO_STORE_CACHE_CONTROL,
"a document fetched from an index upstream was served as cacheable"
);
assert_eq!(
response
.headers()
.get("cdn-cache-control")
.expect("the edge was never told to skip storing this document")
.to_str()
.unwrap(),
"no-store"
);
let served = read_document(response).await;
assert!(
served.contains(r#"src="/assets/app.js?_="#),
"an index upstream served its assets without a cache-busting query"
);
}
#[tokio::test]
async fn the_configured_static_cdn_stands_in_when_discovery_names_none() {
let state =
spa_state_without_discovered_endpoints(Some("https://fallbackcdn.example.test")).await;
let response = serve_spa_index(&state, &HeaderMap::new(), "/channels/@me").await;
assert_eq!(response.status(), StatusCode::OK);
let served = read_document(response).await;
assert!(
served.contains(
r#"<link rel="preconnect" href="https://fallbackcdn.example.test" crossorigin>"#
),
"the configured static CDN never reached the anonymous preconnect"
);
assert!(
served.contains(r#"<link rel="preconnect" href="https://fallbackcdn.example.test">"#),
"the configured static CDN never reached the credentialed preconnect"
);
assert!(
served.contains(r#"href="https://fallbackcdn.example.test/web/favicon-32x32.png""#),
"the favicon was not resolved against the configured static CDN"
);
assert!(!served.contains("{{STATIC_CDN_ENDPOINT}}"));
assert_eq!(
served.matches("preconnect").count(),
2,
"a media endpoint nobody named still warmed a socket"
);
}
#[tokio::test]
async fn an_endpoint_neither_discovered_nor_configured_warms_no_socket_at_all() {
let state = spa_state_without_discovered_endpoints(None).await;
let response = serve_spa_index(&state, &HeaderMap::new(), "/channels/@me").await;
assert_eq!(response.status(), StatusCode::OK);
let served = read_document(response).await;
assert_eq!(
served.matches("preconnect").count(),
0,
"an endpoint nobody named still reached the served document"
);
assert!(!served.contains("{{STATIC_CDN_ENDPOINT}}"));
assert!(!served.contains("{{MEDIA_ENDPOINT}}"));
assert!(
served.contains(r#"href="/web/favicon-32x32.png""#),
"an unnamed static CDN left the favicon pointing somewhere other than our own origin"
);
}
#[test] #[test]
fn font_mime_types_are_cors_enabled() { fn font_mime_types_are_cors_enabled() {
assert!(is_font_mime("font/woff2")); assert!(is_font_mime("font/woff2"));
+38 -1
View File
@@ -240,13 +240,16 @@ pub fn is_font_mime(mime_type: &str) -> bool {
) )
} }
pub const LONG_LIVED_ASSET_CACHE_CONTROL: &str = "public, max-age=31536000, immutable";
pub const REVALIDATED_ASSET_CACHE_CONTROL: &str = "public, max-age=3600, must-revalidate";
pub fn is_hashed_asset(path: &str) -> bool { pub fn is_hashed_asset(path: &str) -> bool {
let filename = path.rsplit('/').next().unwrap_or(path); let filename = path.rsplit('/').next().unwrap_or(path);
let Some(last_dot) = filename.rfind('.') else { let Some(last_dot) = filename.rfind('.') else {
return false; return false;
}; };
let stem = &filename[..last_dot]; let stem = &filename[..last_dot];
if is_content_hash(stem) { if stem.split('.').next().is_some_and(is_content_hash) {
return true; return true;
} }
['.', '-'].iter().any(|sep| { ['.', '-'].iter().any(|sep| {
@@ -255,6 +258,14 @@ pub fn is_hashed_asset(path: &str) -> bool {
}) })
} }
pub fn asset_cache_control(path: &str) -> &'static str {
if is_hashed_asset(path) {
LONG_LIVED_ASSET_CACHE_CONTROL
} else {
REVALIDATED_ASSET_CACHE_CONTROL
}
}
fn is_content_hash(value: &str) -> bool { fn is_content_hash(value: &str) -> bool {
value.len() >= 8 && value.chars().all(|c| c.is_ascii_hexdigit()) value.len() >= 8 && value.chars().all(|c| c.is_ascii_hexdigit())
} }
@@ -345,10 +356,36 @@ mod tests {
assert!(is_hashed_asset("/assets/488b87159423ca35.js")); assert!(is_hashed_asset("/assets/488b87159423ca35.js"));
} }
#[test]
fn hashed_asset_accepts_the_contenthash_worker_bundle_name() {
assert!(
is_hashed_asset("assets/2d715e4730758083.worker.js"),
"rspack emits workers as assets/[contenthash:16].worker.js"
);
}
#[test] #[test]
fn hashed_asset_negative() { fn hashed_asset_negative() {
assert!(!is_hashed_asset("app.js")); assert!(!is_hashed_asset("app.js"));
assert!(!is_hashed_asset("style.css")); assert!(!is_hashed_asset("style.css"));
assert!(!is_hashed_asset("a79f1c3119cd700d/app.js")); assert!(!is_hashed_asset("a79f1c3119cd700d/app.js"));
} }
#[test]
fn the_bundled_font_licences_are_not_treated_as_content_hashed() {
assert!(!is_hashed_asset("assets/fonts-NOTICE.txt"));
assert!(!is_hashed_asset("assets/fonts-LICENSE-IBM-PLEX.txt"));
}
#[test]
fn only_a_content_hashed_asset_is_promised_to_never_change() {
assert_eq!(
asset_cache_control("assets/469e0b8f10c496a1.css"),
LONG_LIVED_ASSET_CACHE_CONTROL
);
assert_eq!(
asset_cache_control("assets/fonts-NOTICE.txt"),
REVALIDATED_ASSET_CACHE_CONTROL
);
}
} }