diff --git a/fluxer_app_proxy/src/routes/spa_index.rs b/fluxer_app_proxy/src/routes/spa_index.rs index 041a390ae..44dffcd3a 100644 --- a/fluxer_app_proxy/src/routes/spa_index.rs +++ b/fluxer_app_proxy/src/routes/spa_index.rs @@ -167,6 +167,11 @@ async fn serve_spa_index(state: &AppState, headers: &HeaderMap) -> Response { Ok(content) => content, Err(response) => return response, }; + let raw_html = if is_self_hosted(&discovery) { + strip_link_preview_metadata(&raw_html) + } else { + raw_html + }; let dev_buster = should_bust_dev_assets.then(current_dev_asset_cache_buster); let html = match render_spa_document( @@ -234,6 +239,36 @@ fn render_spa_document( Ok(document) } +fn is_self_hosted(discovery: &DiscoveryResponse) -> bool { + discovery + .data + .get("features") + .and_then(|features| features.get("self_hosted")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false) +} + +fn strip_link_preview_metadata(html: &str) -> String { + let html = remove_elements(html, ""); + remove_elements(&html, r#"") +} + +fn remove_elements(html: &str, start: &str, end: &str) -> String { + let mut rest = html; + let mut output = String::with_capacity(html.len()); + + while let Some(index) = rest.find(start) { + let Some(length) = rest[index..].find(end) else { + break; + }; + output.push_str(&rest[..index]); + rest = rest[index + length + end.len()..].trim_start_matches('\n'); + } + + output.push_str(rest); + output +} + async fn refresh_discovery_for_spa(state: &AppState) -> Option { state .discovery_cache @@ -722,6 +757,57 @@ mod tests { const DISCOVERY_BODY_WITHOUT_ENDPOINTS: &str = r#"{"api_code_version":"proxy-test"}"#; + const DISCOVERY_BODY_SELF_HOSTED: &str = r#"{"api_code_version":"proxy-test","endpoints":{"static_cdn":"https://cdn.example.test","media":"https://media.example.test"},"features":{"self_hosted":true}}"#; + + const SHIPPED_APP_SHELL: &str = include_str!("../../../fluxer_app/index.html"); + + #[test] + fn the_shipped_shell_loses_every_link_preview_field_when_stripped() { + assert!(SHIPPED_APP_SHELL.contains("")); + assert!(SHIPPED_APP_SHELL.contains(r#"<meta name="description""#)); + + let stripped = strip_link_preview_metadata(SHIPPED_APP_SHELL); + + assert!(!stripped.contains("<title")); + assert!(!stripped.contains(r#"name="description""#)); + assert!(!stripped.contains("og:")); + assert!(!stripped.contains("twitter:")); + assert!(stripped.contains(r#"<meta name="viewport""#)); + assert!(stripped.contains("<!--{{FLUXER_BOOTSTRAP}}-->")); + } + + #[tokio::test] + async fn a_self_hosted_instance_serves_no_link_preview_metadata() { + let state = assemble_spa_state( + ReleaseChannel::Stable, + Some(SHIPPED_APP_SHELL), + DISCOVERY_BODY_SELF_HOSTED, + None, + None, + ) + .await; + + let response = serve_spa_index(&state, &HeaderMap::new()).await; + assert_eq!(response.status(), StatusCode::OK); + let served = read_document(response).await; + + assert!(!served.contains("<title")); + assert!(!served.contains(r#"name="description""#)); + assert!(served.contains("window.__FLUXER_BOOTSTRAP__")); + } + + #[tokio::test] + async fn the_official_instance_keeps_its_link_preview_metadata() { + let state = spa_state_serving(ReleaseChannel::Stable, Some(SHIPPED_APP_SHELL)).await; + + let response = serve_spa_index(&state, &HeaderMap::new()).await; + assert_eq!(response.status(), StatusCode::OK); + let served = read_document(response).await; + + assert!(served.contains("<title>Fluxer")); + assert!(served.contains(r#" String { let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap();