mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(web): prepare the fluxer.com domain migration (#2952)
This commit is contained in:
@@ -71,6 +71,27 @@ pub fn build_bootstrap_script(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn rewrite_endpoints_for_same_origin_host(instance: &mut serde_json::Value, host: &str) {
|
||||
let Some(endpoints) = instance
|
||||
.get_mut("endpoints")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let origin = format!("https://{host}");
|
||||
let api = format!("{origin}/api");
|
||||
for (key, value) in [
|
||||
("api_client", &api),
|
||||
("api", &api),
|
||||
("webapp", &origin),
|
||||
("app", &origin),
|
||||
] {
|
||||
if let Some(endpoint) = endpoints.get_mut(key) {
|
||||
*endpoint = serde_json::Value::String(value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn api_public_endpoint<'a>(
|
||||
configured: Option<&'a str>,
|
||||
discovery: &'a DiscoveryResponse,
|
||||
@@ -541,6 +562,50 @@ mod tests {
|
||||
assert_eq!(api_public_endpoint(None, &discovery), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_same_origin_host_takes_over_the_client_and_web_app_endpoints() {
|
||||
let mut instance = serde_json::json!({
|
||||
"endpoints": {
|
||||
"api": "https://web.fluxer.app/api",
|
||||
"api_client": "https://web.fluxer.app/api",
|
||||
"api_public": "https://api.fluxer.app",
|
||||
"gateway": "wss://gateway.fluxer.app",
|
||||
"webapp": "https://web.fluxer.app",
|
||||
"app": "https://web.fluxer.app",
|
||||
"marketing": "https://fluxer.app"
|
||||
}
|
||||
});
|
||||
rewrite_endpoints_for_same_origin_host(&mut instance, "fluxer.com");
|
||||
assert_eq!(
|
||||
instance["endpoints"],
|
||||
serde_json::json!({
|
||||
"api": "https://fluxer.com/api",
|
||||
"api_client": "https://fluxer.com/api",
|
||||
"api_public": "https://api.fluxer.app",
|
||||
"gateway": "wss://gateway.fluxer.app",
|
||||
"webapp": "https://fluxer.com",
|
||||
"app": "https://fluxer.com",
|
||||
"marketing": "https://fluxer.app"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_same_origin_host_never_invents_endpoints_discovery_left_out() {
|
||||
let mut instance = serde_json::json!({
|
||||
"endpoints": {"api_client": "https://web.fluxer.app/api"}
|
||||
});
|
||||
rewrite_endpoints_for_same_origin_host(&mut instance, "fluxer.com");
|
||||
assert_eq!(
|
||||
instance,
|
||||
serde_json::json!({"endpoints": {"api_client": "https://fluxer.com/api"}})
|
||||
);
|
||||
|
||||
let mut without_endpoints = serde_json::json!({"name": "test"});
|
||||
rewrite_endpoints_for_same_origin_host(&mut without_endpoints, "fluxer.com");
|
||||
assert_eq!(without_endpoints, serde_json::json!({"name": "test"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_boot_script_hands_the_repaired_endpoint_to_both_globals() {
|
||||
let discovery = discovery_offering("https://chat.example.test:8443/api");
|
||||
|
||||
@@ -356,6 +356,8 @@ pub struct AppProxyConfig {
|
||||
pub geoip_s3_config: Option<GeoipS3Config>,
|
||||
pub trust_client_ip_header: bool,
|
||||
pub client_ip_header_name: String,
|
||||
pub same_origin_hosts: Vec<String>,
|
||||
pub manifest_scope_extensions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -511,10 +513,101 @@ impl AppProxyConfig {
|
||||
)
|
||||
.trim()
|
||||
.to_ascii_lowercase(),
|
||||
same_origin_hosts: parse_same_origin_hosts(
|
||||
"FLUXER_APP_PROXY_SAME_ORIGIN_HOSTS",
|
||||
&cfg::read_env("FLUXER_APP_PROXY_SAME_ORIGIN_HOSTS", ""),
|
||||
),
|
||||
manifest_scope_extensions: parse_manifest_scope_extensions(
|
||||
"FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS",
|
||||
&cfg::read_env("FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS", ""),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_manifest_scope_extensions(name: &'static str, raw: &str) -> Vec<String> {
|
||||
let mut origins: Vec<String> = Vec::new();
|
||||
for value in raw
|
||||
.split([',', ' ', '\t', '\n'])
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
match parse_manifest_scope_extension_origin(name, value) {
|
||||
Ok(origin) => {
|
||||
if !origins.contains(&origin) {
|
||||
origins.push(origin);
|
||||
}
|
||||
}
|
||||
Err(error) => warn_invalid(&error),
|
||||
}
|
||||
}
|
||||
origins
|
||||
}
|
||||
|
||||
fn parse_manifest_scope_extension_origin(
|
||||
name: &'static str,
|
||||
value: &str,
|
||||
) -> Result<String, InvalidAppProxyEnvironmentError> {
|
||||
let origin = Url::parse(value).ok().filter(|url| {
|
||||
url.scheme() == "https"
|
||||
&& url.host_str().is_some()
|
||||
&& url.username().is_empty()
|
||||
&& url.password().is_none()
|
||||
&& url.path() == "/"
|
||||
&& url.query().is_none()
|
||||
&& url.fragment().is_none()
|
||||
});
|
||||
match origin {
|
||||
Some(url) => Ok(url.origin().ascii_serialization()),
|
||||
None => Err(InvalidAppProxyEnvironmentError::new(
|
||||
name,
|
||||
value,
|
||||
"an HTTPS origin without a path, query or credentials",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_same_origin_hosts(name: &'static str, raw: &str) -> Vec<String> {
|
||||
let mut hosts: Vec<String> = Vec::new();
|
||||
for value in raw
|
||||
.split([',', ' ', '\t', '\n'])
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
match parse_same_origin_host(name, value) {
|
||||
Ok(host) => {
|
||||
if !hosts.contains(&host) {
|
||||
hosts.push(host);
|
||||
}
|
||||
}
|
||||
Err(error) => warn_invalid(&error),
|
||||
}
|
||||
}
|
||||
hosts
|
||||
}
|
||||
|
||||
fn parse_same_origin_host(
|
||||
name: &'static str,
|
||||
value: &str,
|
||||
) -> Result<String, InvalidAppProxyEnvironmentError> {
|
||||
let host = value.trim_end_matches('.').to_ascii_lowercase();
|
||||
let is_hostname = !host.is_empty()
|
||||
&& Url::parse(&format!("https://{host}/")).is_ok_and(|url| {
|
||||
url.host_str() == Some(host.as_str())
|
||||
&& url.port().is_none()
|
||||
&& url.path() == "/"
|
||||
&& url.username().is_empty()
|
||||
});
|
||||
if !is_hostname {
|
||||
return Err(InvalidAppProxyEnvironmentError::new(
|
||||
name,
|
||||
value,
|
||||
"a bare hostname without a scheme, port or path",
|
||||
));
|
||||
}
|
||||
Ok(host)
|
||||
}
|
||||
|
||||
fn resolve_discovery_upstream_url_from_env() -> String {
|
||||
resolve_discovery_upstream_url(|name| env::var(name).ok())
|
||||
}
|
||||
@@ -699,6 +792,65 @@ mod tests {
|
||||
assert!(error.to_string().contains("FLUXER_PUBLIC_ORIGIN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_origin_hosts_default_to_none() {
|
||||
assert!(parse_same_origin_hosts("TEST_SAME_ORIGIN_HOSTS", "").is_empty());
|
||||
assert!(parse_same_origin_hosts("TEST_SAME_ORIGIN_HOSTS", " , ").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_origin_hosts_are_normalised_and_deduplicated() {
|
||||
assert_eq!(
|
||||
parse_same_origin_hosts(
|
||||
"TEST_SAME_ORIGIN_HOSTS",
|
||||
" web.fluxer.app, Fluxer.COM,fluxer.com. ,fluxer.com"
|
||||
),
|
||||
vec!["web.fluxer.app".to_owned(), "fluxer.com".to_owned()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_origin_hosts_drop_anything_but_a_bare_hostname() {
|
||||
assert_eq!(
|
||||
parse_same_origin_hosts(
|
||||
"TEST_SAME_ORIGIN_HOSTS",
|
||||
"https://fluxer.com,fluxer.com:443,fluxer.com/api,[email protected],canary.fluxer.com"
|
||||
),
|
||||
vec!["canary.fluxer.com".to_owned()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_scope_extensions_default_to_none() {
|
||||
assert!(parse_manifest_scope_extensions("TEST_SCOPE_EXTENSIONS", "").is_empty());
|
||||
assert!(parse_manifest_scope_extensions("TEST_SCOPE_EXTENSIONS", " , ").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_scope_extensions_are_normalised_and_deduplicated() {
|
||||
assert_eq!(
|
||||
parse_manifest_scope_extensions(
|
||||
"TEST_SCOPE_EXTENSIONS",
|
||||
" https://Fluxer.COM, https://fluxer.com/ ,https://canary.fluxer.com:443"
|
||||
),
|
||||
vec![
|
||||
"https://fluxer.com".to_owned(),
|
||||
"https://canary.fluxer.com".to_owned()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_scope_extensions_drop_anything_but_an_https_origin() {
|
||||
assert_eq!(
|
||||
parse_manifest_scope_extensions(
|
||||
"TEST_SCOPE_EXTENSIONS",
|
||||
"fluxer.com,http://fluxer.com,https://fluxer.com/app,https://fluxer.com/?a=1,https://[email protected],https://canary.fluxer.com"
|
||||
),
|
||||
vec!["https://canary.fluxer.com".to_owned()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csp_config_default_has_no_extra_sources() {
|
||||
let c = CspConfig::default();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::bootstrap::{build_bootstrap_script, inject_bootstrap};
|
||||
use crate::config::HttpEndpoint;
|
||||
use crate::bootstrap::{
|
||||
build_bootstrap_script, inject_bootstrap, rewrite_endpoints_for_same_origin_host,
|
||||
};
|
||||
use crate::config::{AppProxyConfig, HttpEndpoint};
|
||||
use crate::csp::{RuntimeCspSources, generate_nonce};
|
||||
use crate::discovery_cache::{DiscoveryResponse, discovery_endpoint};
|
||||
use crate::geoip::build_geoip_response;
|
||||
@@ -141,13 +143,16 @@ async fn serve_static_file(
|
||||
async fn serve_spa_index(state: &AppState, headers: &HeaderMap) -> Response {
|
||||
let should_bust_dev_assets = state.config.index_upstream_url.is_some();
|
||||
|
||||
let discovery = match refresh_discovery_for_spa(state).await {
|
||||
let mut discovery = match refresh_discovery_for_spa(state).await {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
tracing::error!("discovery cache empty, cannot serve SPA");
|
||||
return StatusCode::SERVICE_UNAVAILABLE.into_response();
|
||||
}
|
||||
};
|
||||
if let Some(host) = same_origin_host(&state.config, headers) {
|
||||
rewrite_endpoints_for_same_origin_host(&mut discovery.data, host);
|
||||
}
|
||||
|
||||
let nonce = generate_nonce();
|
||||
let runtime_csp_sources = build_runtime_csp_sources(state, &discovery);
|
||||
@@ -192,6 +197,29 @@ async fn serve_spa_index(state: &AppState, headers: &HeaderMap) -> Response {
|
||||
build_spa_response(html, csp, should_bust_dev_assets)
|
||||
}
|
||||
|
||||
fn same_origin_host<'a>(config: &'a AppProxyConfig, headers: &HeaderMap) -> Option<&'a str> {
|
||||
if config.same_origin_hosts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let host = request_hostname(headers.get(header::HOST)?.to_str().ok()?)?;
|
||||
config
|
||||
.same_origin_hosts
|
||||
.iter()
|
||||
.find(|candidate| candidate.eq_ignore_ascii_case(host))
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
fn request_hostname(authority: &str) -> Option<&str> {
|
||||
let authority = authority.trim();
|
||||
let hostname = if authority.starts_with('[') {
|
||||
&authority[..=authority.find(']')?]
|
||||
} else {
|
||||
authority.split(':').next()?
|
||||
};
|
||||
let hostname = hostname.trim_end_matches('.');
|
||||
(!hostname.is_empty()).then_some(hostname)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SpaDocumentSizeLimitError {
|
||||
attempted_bytes: usize,
|
||||
@@ -432,6 +460,10 @@ fn build_spa_response(html: Box<str>, csp: HeaderValue, dev_no_store: bool) -> R
|
||||
headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
|
||||
}
|
||||
super::set_security_headers(headers);
|
||||
headers.insert(
|
||||
HeaderName::from_static("x-fluxer-app-shell"),
|
||||
HeaderValue::from_static("1"),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::HeaderName::from_static("accept-ch"),
|
||||
HeaderValue::from_static(ACCEPT_CH_VALUE),
|
||||
@@ -755,6 +787,8 @@ mod tests {
|
||||
|
||||
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_WITH_WEB_APP_ENDPOINTS: &str = r#"{"api_code_version":"proxy-test","endpoints":{"api":"https://web.fluxer.app/api","api_client":"https://web.fluxer.app/api","api_public":"https://api.fluxer.app","webapp":"https://web.fluxer.app","static_cdn":"https://cdn.example.test","media":"https://media.example.test"}}"#;
|
||||
|
||||
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}}"#;
|
||||
@@ -924,6 +958,100 @@ mod tests {
|
||||
String::from_utf8(bytes.to_vec()).unwrap()
|
||||
}
|
||||
|
||||
async fn spa_state_with_same_origin_hosts(hosts: &[&str]) -> AppState {
|
||||
let mut state = assemble_spa_state(
|
||||
ReleaseChannel::Stable,
|
||||
Some(SHELL_WITH_ENDPOINT_HOLES),
|
||||
DISCOVERY_BODY_WITH_WEB_APP_ENDPOINTS,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut config = (*state.config).clone();
|
||||
config.same_origin_hosts = hosts.iter().map(|host| (*host).to_owned()).collect();
|
||||
state.config = Arc::new(config);
|
||||
state
|
||||
}
|
||||
|
||||
fn request_from_host(host: &str) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::HOST, HeaderValue::from_str(host).unwrap());
|
||||
headers
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_request_hostname_drops_its_port_and_trailing_dot() {
|
||||
assert_eq!(request_hostname("fluxer.com"), Some("fluxer.com"));
|
||||
assert_eq!(request_hostname("Fluxer.com:443"), Some("Fluxer.com"));
|
||||
assert_eq!(request_hostname("fluxer.com.:8443"), Some("fluxer.com"));
|
||||
assert_eq!(request_hostname("[::1]:8080"), Some("[::1]"));
|
||||
assert_eq!(request_hostname(":443"), None);
|
||||
assert_eq!(request_hostname("[::1"), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_listed_host_gets_same_origin_endpoints_in_its_bootstrap() {
|
||||
let state = spa_state_with_same_origin_hosts(&["web.fluxer.app", "fluxer.com"]).await;
|
||||
|
||||
let response = serve_spa_index(&state, &request_from_host("FLUXER.com:443")).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let served = read_document(response).await;
|
||||
assert!(served.contains(r#""api_client":"https://fluxer.com/api""#));
|
||||
assert!(served.contains(r#""api":"https://fluxer.com/api""#));
|
||||
assert!(served.contains(r#""webapp":"https://fluxer.com""#));
|
||||
assert!(served.contains(r#""api_public":"https://api.fluxer.app""#));
|
||||
assert!(!served.contains("https://web.fluxer.app"));
|
||||
|
||||
let cached = state.discovery_cache.get().await.unwrap();
|
||||
assert_eq!(
|
||||
cached.data["endpoints"]["api_client"], "https://web.fluxer.app/api",
|
||||
"the shared discovery snapshot was rewritten for one request"
|
||||
);
|
||||
|
||||
let response = serve_spa_index(&state, &request_from_host("web.fluxer.app")).await;
|
||||
let served = read_document(response).await;
|
||||
assert!(served.contains(r#""api_client":"https://web.fluxer.app/api""#));
|
||||
assert!(!served.contains("https://fluxer.com"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_unlisted_host_keeps_the_discovered_endpoints() {
|
||||
let state = spa_state_with_same_origin_hosts(&["fluxer.com"]).await;
|
||||
|
||||
for headers in [request_from_host("evil.example"), HeaderMap::new()] {
|
||||
let response = serve_spa_index(&state, &headers).await;
|
||||
let served = read_document(response).await;
|
||||
assert!(served.contains(r#""api_client":"https://web.fluxer.app/api""#));
|
||||
assert!(served.contains(r#""webapp":"https://web.fluxer.app""#));
|
||||
assert!(!served.contains("https://fluxer.com"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_host_is_rewritten_when_none_is_configured() {
|
||||
let state = spa_state_with_same_origin_hosts(&[]).await;
|
||||
|
||||
let response = serve_spa_index(&state, &request_from_host("fluxer.com")).await;
|
||||
let served = read_document(response).await;
|
||||
assert!(served.contains(r#""api_client":"https://web.fluxer.app/api""#));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_spa_document_marks_itself_as_the_app_shell() {
|
||||
let state =
|
||||
spa_state_serving(ReleaseChannel::Canary, Some(SHELL_WITH_ENDPOINT_HOLES)).await;
|
||||
|
||||
let response = serve_spa_index(&state, &HeaderMap::new()).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-fluxer-app-shell")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("1")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_live_branch_serves_a_rendered_document_and_not_the_raw_shell() {
|
||||
let state =
|
||||
|
||||
@@ -26,15 +26,46 @@ pub async fn version_json(State(state): State<AppState>) -> Response {
|
||||
|
||||
pub async fn manifest_json(State(state): State<AppState>) -> Response {
|
||||
let static_cdn_endpoint = runtime_static_cdn_endpoint(&state).await;
|
||||
serve_static_text_file_with_cdn(
|
||||
serve_static_text_file_with_substitutions(
|
||||
&state,
|
||||
"manifest.json",
|
||||
"application/manifest+json",
|
||||
static_cdn_endpoint.as_ref(),
|
||||
Some(&state.config.manifest_scope_extensions),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn with_scope_extensions(text: String, origins: &[String]) -> String {
|
||||
if origins.is_empty() {
|
||||
return text;
|
||||
}
|
||||
let Ok(serde_json::Value::Object(mut manifest)) = serde_json::from_str(&text) else {
|
||||
return text;
|
||||
};
|
||||
let scope_extensions = origins
|
||||
.iter()
|
||||
.map(|origin| serde_json::json!({ "type": "origin", "origin": origin }))
|
||||
.collect();
|
||||
manifest.insert(
|
||||
"scope_extensions".to_owned(),
|
||||
serde_json::Value::Array(scope_extensions),
|
||||
);
|
||||
serde_json::to_string_pretty(&manifest).unwrap_or(text)
|
||||
}
|
||||
|
||||
fn substitute_placeholders(
|
||||
text: &str,
|
||||
static_cdn_endpoint: &str,
|
||||
scope_extensions: Option<&[String]>,
|
||||
) -> String {
|
||||
let text = text.replace("{{STATIC_CDN_ENDPOINT}}", static_cdn_endpoint);
|
||||
match scope_extensions {
|
||||
Some(origins) => with_scope_extensions(text, origins),
|
||||
None => text,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn browserconfig_xml(State(state): State<AppState>) -> Response {
|
||||
let static_cdn_endpoint = runtime_static_cdn_endpoint(&state).await;
|
||||
serve_static_text_file_with_cdn(
|
||||
@@ -73,6 +104,23 @@ async fn serve_static_text_file_with_cdn(
|
||||
filename: &str,
|
||||
content_type: &str,
|
||||
static_cdn_endpoint: Option<&HttpEndpoint>,
|
||||
) -> Response {
|
||||
serve_static_text_file_with_substitutions(
|
||||
state,
|
||||
filename,
|
||||
content_type,
|
||||
static_cdn_endpoint,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn serve_static_text_file_with_substitutions(
|
||||
state: &AppState,
|
||||
filename: &str,
|
||||
content_type: &str,
|
||||
static_cdn_endpoint: Option<&HttpEndpoint>,
|
||||
scope_extensions: Option<&[String]>,
|
||||
) -> Response {
|
||||
let static_dir = state.config.static_dir.as_str();
|
||||
let file_path = Path::new(static_dir).join(filename);
|
||||
@@ -104,8 +152,7 @@ async fn serve_static_text_file_with_cdn(
|
||||
|
||||
let replacement = static_cdn_endpoint.map_or("", HttpEndpoint::as_str);
|
||||
let body: axum::body::Body = match std::str::from_utf8(&content) {
|
||||
Ok(text) => text
|
||||
.replace("{{STATIC_CDN_ENDPOINT}}", replacement)
|
||||
Ok(text) => substitute_placeholders(text, replacement, scope_extensions)
|
||||
.into_bytes()
|
||||
.into(),
|
||||
Err(_) => content.into(),
|
||||
@@ -201,6 +248,80 @@ fn is_content_hash(value: &str) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const BUILT_MANIFEST: &str = r#"{
|
||||
"id": "/",
|
||||
"start_url": "/app",
|
||||
"scope": "/",
|
||||
"scope_extensions": [],
|
||||
"icons": [
|
||||
{
|
||||
"src": "{{STATIC_CDN_ENDPOINT}}/web/android-chrome-192x192.png"
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
fn substituted_manifest(origins: &[&str]) -> serde_json::Value {
|
||||
let origins: Vec<String> = origins.iter().map(|origin| (*origin).to_owned()).collect();
|
||||
let text =
|
||||
substitute_placeholders(BUILT_MANIFEST, "https://fluxerstatic.com", Some(&origins));
|
||||
serde_json::from_str(&text).expect("substituted manifest must stay valid JSON")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_scope_extensions_default_to_the_built_empty_list() {
|
||||
let manifest = substituted_manifest(&[]);
|
||||
assert_eq!(manifest["scope_extensions"], serde_json::json!([]));
|
||||
assert_eq!(
|
||||
manifest["icons"][0]["src"],
|
||||
"https://fluxerstatic.com/web/android-chrome-192x192.png"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_scope_extensions_list_each_configured_origin() {
|
||||
let manifest = substituted_manifest(&["https://fluxer.com", "https://canary.fluxer.com"]);
|
||||
assert_eq!(
|
||||
manifest["scope_extensions"],
|
||||
serde_json::json!([
|
||||
{ "type": "origin", "origin": "https://fluxer.com" },
|
||||
{ "type": "origin", "origin": "https://canary.fluxer.com" }
|
||||
])
|
||||
);
|
||||
assert_eq!(manifest["id"], "/");
|
||||
assert_eq!(manifest["start_url"], "/app");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_scope_extensions_are_added_when_the_build_has_none() {
|
||||
let text = substitute_placeholders(
|
||||
r#"{"id":"/"}"#,
|
||||
"",
|
||||
Some(&["https://fluxer.com".to_owned()]),
|
||||
);
|
||||
let manifest: serde_json::Value = serde_json::from_str(&text).expect("valid JSON");
|
||||
assert_eq!(
|
||||
manifest["scope_extensions"],
|
||||
serde_json::json!([{ "type": "origin", "origin": "https://fluxer.com" }])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_text_files_are_left_alone() {
|
||||
let origins = ["https://fluxer.com".to_owned()];
|
||||
assert_eq!(
|
||||
substitute_placeholders(
|
||||
"not json {{STATIC_CDN_ENDPOINT}}",
|
||||
"https://cdn",
|
||||
Some(&origins)
|
||||
),
|
||||
"not json https://cdn"
|
||||
);
|
||||
assert_eq!(
|
||||
substitute_placeholders(BUILT_MANIFEST, "", None),
|
||||
BUILT_MANIFEST.replace("{{STATIC_CDN_ENDPOINT}}", "")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mime_html() {
|
||||
assert_eq!(guess_mime("i.html"), "text/html; charset=utf-8");
|
||||
|
||||
Reference in New Issue
Block a user