diff --git a/rust/agama-server/src/web/http.rs b/rust/agama-server/src/web/http.rs index dce25e517e..1aa010a512 100644 --- a/rust/agama-server/src/web/http.rs +++ b/rust/agama-server/src/web/http.rs @@ -5,11 +5,13 @@ use super::{ state::ServiceState, }; use axum::{ + body::Body, extract::State, - http::{header::SET_COOKIE, HeaderMap}, + http::{header, HeaderMap, HeaderValue, StatusCode}, response::IntoResponse, Json, }; +use axum_extra::extract::cookie::CookieJar; use pam::Client; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -62,7 +64,7 @@ pub async fn login( let mut headers = HeaderMap::new(); let cookie = format!("agamaToken={}; HttpOnly", &token); headers.insert( - SET_COOKIE, + header::SET_COOKIE, cookie.parse().expect("could not build a valid cookie"), ); @@ -76,7 +78,7 @@ pub async fn logout(_claims: TokenClaims) -> Result Result Result<(), AuthError> { Ok(()) } + +// builds a response tuple for translation redirection +fn redirect_to_file(file: &str) -> (StatusCode, HeaderMap, Body) { + tracing::info!("Redirecting to translation file: {}", file); + + let mut response_headers = HeaderMap::new(); + // translation found, redirect to the real file + response_headers.insert( + header::LOCATION, + // if the file exists then the name is a valid value and unwrapping is safe + HeaderValue::from_str(file).unwrap(), + ); + + ( + StatusCode::TEMPORARY_REDIRECT, + response_headers, + Body::empty(), + ) +} + +// handle the /po.js request +// the requested language (locale) is sent in the "agamaLang" HTTP cookie +// this reimplements the Cockpit translation support +pub async fn po(State(state): State, jar: CookieJar) -> impl IntoResponse { + if let Some(cookie) = jar.get("agamaLang") { + tracing::info!("Language cookie: {}", cookie.value()); + // try parsing the cookie + if let Some((lang, region)) = cookie.value().split_once('-') { + // first try language + country + let target_file = format!("po.{}_{}.js", lang, region.to_uppercase()); + if state.public_dir.join(&target_file).exists() { + return redirect_to_file(&target_file); + } else { + // then try the language only + let target_file = format!("po.{}.js", lang); + if state.public_dir.join(&target_file).exists() { + return redirect_to_file(&target_file); + }; + } + } else { + // use the cookie as is + let target_file = format!("po.{}.js", cookie.value()); + if state.public_dir.join(&target_file).exists() { + return redirect_to_file(&target_file); + } + } + } + + tracing::info!("Translation not found"); + // fallback, return empty javascript translations if the language is not supported + let mut response_headers = HeaderMap::new(); + response_headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static("text/javascript"), + ); + + (StatusCode::OK, response_headers, Body::empty()) +} diff --git a/rust/agama-server/src/web/service.rs b/rust/agama-server/src/web/service.rs index a4cc5a664f..bb00f00200 100644 --- a/rust/agama-server/src/web/service.rs +++ b/rust/agama-server/src/web/service.rs @@ -1,15 +1,12 @@ use super::http::{login, logout, session}; use super::{auth::TokenClaims, config::ServiceConfig, state::ServiceState, EventsSender}; use axum::{ - body::Body, extract::Request, - http::{header, HeaderMap, HeaderValue, StatusCode}, middleware, response::IntoResponse, routing::{get, post}, Router, }; -use axum_extra::extract::cookie::CookieJar; use std::{ convert::Infallible, path::{Path, PathBuf}, @@ -77,6 +74,7 @@ impl MainServiceBuilder { let state = ServiceState { config: self.config, events: self.events, + public_dir: self.public_dir.clone(), }; let api_router = self @@ -87,65 +85,12 @@ impl MainServiceBuilder { .route("/ping", get(super::http::ping)) .route("/auth", post(login).get(session).delete(logout)); + tracing::info!("Serving static files from {}", self.public_dir.display()); let serve = ServeDir::new(self.public_dir); - // handle the /po.js request - // the requested language (locale) is sent in the "agamaLanguage" HTTP cookie - // this reimplements the Cockpit translation support - async fn po(jar: CookieJar) -> impl IntoResponse { - let mut response_headers = HeaderMap::new(); - - if let Some(cookie) = jar.get("agamaLanguage") { - let mut target_file = String::new(); - let mut found = false; - // FIXME: this does not work, the public_dir setting is not accessible :-/ - // when using something like PathBuf::from("/usr/share/cockpit/agama") here - // it works just fine.... - let prefix = self.public_dir; - - // try parsing the cookie - if let Some((lang, region)) = cookie.value().split_once('-') { - // first try the full locale - target_file = format!("po.{}_{}.js", lang, region.to_uppercase()); - found = prefix.join(&target_file).exists(); - - if !found { - // then try the language only - target_file = format!("po.{}.js", lang); - found = prefix.join(&target_file).exists(); - } - } - - if !found { - // use the full cookie without parsing - target_file = format!("po.{}.js", cookie.value()); - found = prefix.join(&target_file).exists(); - } - - if found { - // translation found, redirect to the real file - response_headers.insert( - header::LOCATION, - // if the file exists then the name is a valid header value and unwrapping is safe - HeaderValue::from_str(&target_file).unwrap() - ); - - return (StatusCode::TEMPORARY_REDIRECT, response_headers, Body::empty()) - } - } - - // fallback, return empty javascript translations if the language is not supported - response_headers.insert( - header::CONTENT_TYPE, - HeaderValue::from_static("text/javascript"), - ); - - (StatusCode::OK, response_headers, Body::empty()) - } - Router::new() .nest_service("/", serve) - .route("/po.js", get(po)) + .route("/po.js", get(super::http::po)) .nest("/api", api_router) .layer(TraceLayer::new_for_http()) .layer(CompressionLayer::new().br(true)) diff --git a/rust/agama-server/src/web/state.rs b/rust/agama-server/src/web/state.rs index c35592b8c5..01cdf6f625 100644 --- a/rust/agama-server/src/web/state.rs +++ b/rust/agama-server/src/web/state.rs @@ -1,6 +1,7 @@ //! Implements the web service state. use super::{config::ServiceConfig, EventsSender}; +use std::path::PathBuf; /// Web service state. /// @@ -9,4 +10,5 @@ use super::{config::ServiceConfig, EventsSender}; pub struct ServiceState { pub config: ServiceConfig, pub events: EventsSender, + pub public_dir: PathBuf, }