diff --git a/rust/Cargo.lock b/rust/Cargo.lock index aa3ca3b46b..33d52732a8 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -508,6 +508,7 @@ dependencies = [ "axum", "axum-core", "bytes", + "cookie", "futures-util", "headers", "http 1.1.0", @@ -867,6 +868,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 739926c0a6..b714ad84b8 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -39,7 +39,7 @@ utoipa = { version = "4.2.0", features = ["axum_extras"] } config = "0.14.0" rand = "0.8.5" jsonwebtoken = "9.2.0" -axum-extra = { version = "0.9.2", features = ["typed-header"] } +axum-extra = { version = "0.9.2", features = ["cookie", "typed-header"] } chrono = { version = "0.4.34", default-features = false, features = [ "now", "std", diff --git a/rust/agama-server/src/web/http.rs b/rust/agama-server/src/web/http.rs index dce25e517e..f95e884090 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 d28ebd3c7d..bb00f00200 100644 --- a/rust/agama-server/src/web/service.rs +++ b/rust/agama-server/src/web/service.rs @@ -74,6 +74,7 @@ impl MainServiceBuilder { let state = ServiceState { config: self.config, events: self.events, + public_dir: self.public_dir.clone(), }; let api_router = self @@ -84,9 +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); + Router::new() .nest_service("/", serve) + .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, }