diff --git a/fluxer_app_proxy/src/csp.rs b/fluxer_app_proxy/src/csp.rs index 9bbd29bb1..357b30414 100644 --- a/fluxer_app_proxy/src/csp.rs +++ b/fluxer_app_proxy/src/csp.rs @@ -10,6 +10,7 @@ pub struct RuntimeCspSources { pub media_endpoint: Option, pub s3_public_endpoint: Option, pub s3_uploads_bucket: Option, + pub branding_image_origins: Vec, } const FRAME_SOURCES: &[&str] = &[ @@ -119,6 +120,9 @@ fn build_csp_directives( let mut img = vec!["'self'".to_owned(), "blob:".to_owned(), "data:".to_owned()]; extend_from(&mut img, config.img_src.as_deref(), IMAGE_SOURCES); extend_runtime_sources(&mut img, runtime_sources, true, true); + for origin in &runtime_sources.branding_image_origins { + push_endpoint_source(&mut img, Some(origin)); + } directives.push(format!("img-src {}", img.join(" "))); let mut media = vec!["'self'".to_owned(), "blob:".to_owned()]; @@ -180,6 +184,20 @@ fn extend_runtime_sources( } } +pub fn http_origin(raw: &str) -> Option { + let url = Url::parse(raw.trim()).ok()?; + let scheme = url.scheme(); + if scheme != "http" && scheme != "https" { + return None; + } + let host = url.host_str()?; + let port = url + .port() + .map(|port| format!(":{port}")) + .unwrap_or_default(); + Some(format!("{scheme}://{host}{port}")) +} + fn push_endpoint_source(target: &mut Vec, endpoint: Option<&str>) { let Some(endpoint) = endpoint else { return; diff --git a/fluxer_app_proxy/src/routes/assets_proxy.rs b/fluxer_app_proxy/src/routes/assets_proxy.rs index cff5f609a..767d05608 100644 --- a/fluxer_app_proxy/src/routes/assets_proxy.rs +++ b/fluxer_app_proxy/src/routes/assets_proxy.rs @@ -118,6 +118,7 @@ pub async fn proxy_assets( media_endpoint: None, s3_public_endpoint: None, s3_uploads_bucket: None, + branding_image_origins: Vec::new(), }, ); if let Ok(value) = HeaderValue::from_str(&asset_csp) { diff --git a/fluxer_app_proxy/src/routes/spa_index.rs b/fluxer_app_proxy/src/routes/spa_index.rs index d9a1d2874..5653fb0b9 100644 --- a/fluxer_app_proxy/src/routes/spa_index.rs +++ b/fluxer_app_proxy/src/routes/spa_index.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use crate::bootstrap::{build_bootstrap_script, inject_bootstrap}; -use crate::csp::{RuntimeCspSources, build_csp, generate_nonce}; +use crate::csp::{RuntimeCspSources, build_csp, generate_nonce, http_origin}; use crate::discovery_cache::DiscoveryResponse; use crate::geoip::build_geoip_response; use crate::invite_meta::{ @@ -186,9 +186,42 @@ fn build_runtime_csp_sources(state: &AppState, discovery: &DiscoveryResponse) -> media_endpoint: discovery_endpoint(discovery, "media"), s3_public_endpoint: state.config.s3_public_endpoint.clone(), s3_uploads_bucket: Some(state.config.s3_uploads_bucket.clone()), + branding_image_origins: branding_image_origins(discovery), } } +const BRANDING_IMAGE_KEYS: &[&str] = &[ + "icon_url", + "symbol_url", + "logo_url", + "wordmark_url", + "favicon_url", +]; + +fn branding_image_origins(discovery: &DiscoveryResponse) -> Vec { + let Some(branding) = discovery + .data + .get("app_public") + .and_then(|app_public| app_public.get("branding")) + else { + return Vec::new(); + }; + let mut origins: Vec = Vec::new(); + for key in BRANDING_IMAGE_KEYS { + let Some(origin) = branding + .get(*key) + .and_then(|value| value.as_str()) + .and_then(http_origin) + else { + continue; + }; + if !origins.contains(&origin) { + origins.push(origin); + } + } + origins +} + fn discovery_endpoint(discovery: &DiscoveryResponse, key: &str) -> Option { discovery .data