This commit is contained in:
Mateusz Gruszczyński
2026-08-30 23:00:29 +02:00
parent 6465ba1125
commit 4054fe3f76
32 changed files with 3722 additions and 3272 deletions
+51 -11
View File
@@ -1,27 +1,60 @@
async fn not_found(State(state): State<AppState>, 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<AppState>, headers: HeaderMap) -> Response {
let base = if !state.config.base_path.is_empty() {
state.config.base_path.clone()
} else {
forwarded_prefix(&headers).unwrap_or_default()
};
let body = INDEX_HTML.replace("__GREE_BASE_PATH__", &base);
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("no-cache"));
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<String> {
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", "no-cache") }
async fn theme_init_js() -> Response { static_response(THEME_INIT_JS, "application/javascript; charset=utf-8", "public, max-age=86400") }
async fn styles_css() -> Response { static_response(STYLES_CSS, "text/css; charset=utf-8", "no-cache") }
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 { static_response(SERVICE_WORKER, "application/javascript; charset=utf-8", "no-cache") }
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")
@@ -42,3 +75,10 @@ fn static_response(body: &'static str, content_type: &'static str, cache: &'stat
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
}
+16 -5
View File
@@ -1,13 +1,24 @@
async fn security_headers(request: Request, next: Next) -> Response {
let is_api = request.uri().path().contains("/api/");
let mut response = next.run(request).await;
let is_html = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.split(';').next().is_some_and(|mime| mime.trim().eq_ignore_ascii_case("text/html")));
let headers = response.headers_mut();
headers.insert(header::HeaderName::from_static("x-content-type-options"), HeaderValue::from_static("nosniff"));
headers.insert(header::HeaderName::from_static("x-frame-options"), HeaderValue::from_static("SAMEORIGIN"));
headers.insert(header::HeaderName::from_static("referrer-policy"), HeaderValue::from_static("same-origin"));
headers.insert(header::HeaderName::from_static("content-security-policy"), HeaderValue::from_static("default-src 'self'; connect-src 'self' ws: wss:; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; object-src 'none'"));
headers.insert(header::HeaderName::from_static("permissions-policy"), HeaderValue::from_static("camera=(), microphone=(), geolocation=()"));
if is_api { headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store")); }
if is_html {
headers.insert(header::HeaderName::from_static("x-frame-options"), HeaderValue::from_static("SAMEORIGIN"));
headers.insert(header::HeaderName::from_static("content-security-policy"), HeaderValue::from_static("default-src 'self'; connect-src 'self' ws: wss:; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; object-src 'none'"));
headers.insert(header::HeaderName::from_static("permissions-policy"), HeaderValue::from_static("camera=(), microphone=(), geolocation=()"));
}
if is_api {
headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
}
response
}