async fn not_found(State(state): State, headers: HeaderMap) -> Response { let base = request_base_path(&state, &headers); let home = if base.is_empty() { "/".to_owned() } else { format!("{base}/") }; let body = NOT_FOUND_HTML .replace("__GREE_BASE_PATH__", &base) .replace("__GREE_HOME_PATH__", &home) .replace("__GREE_THEME_INIT_ASSET__", &format!("{base}{THEME_INIT_ASSET_PATH}")) .replace("__GREE_STYLES_ASSET__", &format!("{base}{STYLES_CSS_ASSET_PATH}")); let mut response = Response::new(Body::from(body)); *response.status_mut() = StatusCode::NOT_FOUND; response.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8")); response.headers_mut().insert(header::CACHE_CONTROL, HeaderValue::from_static("private, no-store, no-cache, must-revalidate")); response } async fn index(State(state): State, headers: HeaderMap) -> Response { let base = request_base_path(&state, &headers); let body = INDEX_HTML .replace("__GREE_BASE_PATH__", &base) .replace("__GREE_APP_ASSET__", &format!("{base}{APP_JS_ASSET_PATH}")) .replace("__GREE_THEME_INIT_ASSET__", &format!("{base}{THEME_INIT_ASSET_PATH}")) .replace("__GREE_STYLES_ASSET__", &format!("{base}{STYLES_CSS_ASSET_PATH}")); let mut response = Response::new(Body::from(body)); response.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8")); response.headers_mut().insert(header::CACHE_CONTROL, HeaderValue::from_static("private, no-store, no-cache, must-revalidate")); response } fn request_base_path(state: &AppState, headers: &HeaderMap) -> String { if !state.config.base_path.is_empty() { state.config.base_path.clone() } else { forwarded_prefix(headers).unwrap_or_default() } } fn forwarded_prefix(headers: &HeaderMap) -> Option { let raw = headers.get("x-forwarded-prefix")?.to_str().ok()?.split(',').next()?.trim(); if raw.is_empty() || raw == "/" { return Some(String::new()); } if raw.contains('?') || raw.contains('#') || raw.split('/').any(|part| matches!(part, "." | "..")) { return None; } Some(format!("/{}", raw.trim_matches('/'))) } async fn app_js() -> Response { static_response(APP_JS, "application/javascript; charset=utf-8", "public, max-age=31536000, immutable") } async fn app_js_legacy() -> Response { static_response(APP_JS, "application/javascript; charset=utf-8", "no-cache") } async fn theme_init_js() -> Response { static_response(THEME_INIT_JS, "application/javascript; charset=utf-8", "public, max-age=31536000, immutable") } async fn theme_init_js_legacy() -> Response { static_response(THEME_INIT_JS, "application/javascript; charset=utf-8", "no-cache") } async fn styles_css() -> Response { static_response(STYLES_CSS, "text/css; charset=utf-8", "public, max-age=31536000, immutable") } async fn styles_css_legacy() -> Response { static_response(STYLES_CSS, "text/css; charset=utf-8", "no-cache") } async fn manifest() -> Response { static_response(MANIFEST, "application/manifest+json", "public, max-age=3600") } async fn service_worker() -> Response { let body = SERVICE_WORKER .replace("__GREE_ASSET_CACHE__", ASSET_BUILD_ID) .replace("__GREE_APP_ASSET__", APP_JS_ASSET_PATH) .replace("__GREE_THEME_INIT_ASSET__", THEME_INIT_ASSET_PATH) .replace("__GREE_STYLES_ASSET__", STYLES_CSS_ASSET_PATH); owned_response(body, "application/javascript; charset=utf-8", "no-cache") } async fn favicon() -> Response { static_response(FAVICON, "image/svg+xml", "public, max-age=86400") } async fn language_index() -> Response { static_response(LANGUAGE_MANIFEST_JSON, "application/json; charset=utf-8", "no-cache") } async fn language_file(Path(file): Path) -> Response { let code = file.strip_suffix(".json").unwrap_or(&file); if let Some((_, body)) = LANGUAGE_ASSETS.iter().find(|(language, _)| *language == code) { return static_response(*body, "application/json; charset=utf-8", "no-cache"); } let mut response = Response::new(Body::from("Language not found")); *response.status_mut() = StatusCode::NOT_FOUND; response.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain; charset=utf-8")); response } fn static_response(body: &'static str, content_type: &'static str, cache: &'static str) -> Response { let mut response = Response::new(Body::from(body)); response.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type)); response.headers_mut().insert(header::CACHE_CONTROL, HeaderValue::from_static(cache)); response } fn owned_response(body: String, content_type: &'static str, cache: &'static str) -> Response { let mut response = Response::new(Body::from(body)); response.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type)); response.headers_mut().insert(header::CACHE_CONTROL, HeaderValue::from_static(cache)); response }