From 2389bca42b2a2fb720c9988b8a068ea551d72dff Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Thu, 4 Jun 2026 13:08:34 +0300 Subject: [PATCH] Fixed image transcoding. Paying attention to EXIF orientation data --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/admin.rs | 84 +++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 287d574..b6f84a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3467,7 +3467,7 @@ dependencies = [ [[package]] name = "web-petting" -version = "0.1.12" +version = "0.1.13" dependencies = [ "base64", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 0ef4c88..a032de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "web-petting" -version = "0.1.13" +version = "0.1.14" edition = "2024" [dependencies] diff --git a/src/admin.rs b/src/admin.rs index 7ee2124..64c2f26 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -6,6 +6,7 @@ use cot::request::extractors::Path; use cot::response::{IntoResponse, Redirect, Response}; use cot::router::{Route, Router}; use cot::session::Session; +use image::ImageDecoder; use image::ImageFormat; use image::ImageReader; use image::codecs::jpeg::JpegEncoder; @@ -19,6 +20,7 @@ use crate::telegram; const SESSION_USER_ID: &str = "user_id"; const SESSION_USER_NAME: &str = "user_name"; +const SESSION_OIDC_STATE: &str = "oidc_state"; const MAX_UPLOADED_IMAGE_DIMENSION: u32 = 1920; const UPLOADED_IMAGE_JPEG_QUALITY: u8 = 82; @@ -117,9 +119,16 @@ fn transcode_uploaded_image(data: &[u8], ext: &str) -> cot::Result Option { serde_json::from_slice(&bytes).ok() } -async fn oidc_start(request: Request, db: Database) -> cot::Result { +fn oidc_state_cookie(value: &str, max_age_seconds: u32) -> String { + format!( + "oidc_state={}; Path=/admin/oidc; HttpOnly; SameSite=Lax; Max-Age={}", + value, max_age_seconds, + ) +} + +fn get_cookie(request: &Request, name: &str) -> Option { + let prefix = format!("{name}="); + request + .headers() + .get("cookie") + .and_then(|v| v.to_str().ok()) + .and_then(|cookies| { + cookies.split(';').find_map(|part| { + let part = part.trim(); + part.strip_prefix(&prefix).map(|v| v.to_string()) + }) + }) +} + +async fn oidc_start(request: Request, session: Session, db: Database) -> cot::Result { let lang = detect_lang(&request); let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?; let client_id = oidc_setting(&db, "oidc_client_id").await?; @@ -666,6 +696,7 @@ async fn oidc_start(request: Request, db: Database) -> cot::Result { }; let state = rand_token(); + session.insert(SESSION_OIDC_STATE, state.clone()).await?; let redirect_uri = format!("{}/admin/oidc/callback", site_domain.trim_end_matches('/')); @@ -677,10 +708,7 @@ async fn oidc_start(request: Request, db: Database) -> cot::Result { urlencoding::encode(&state), ); - let state_cookie = format!( - "oidc_state={}; Path=/admin/oidc; HttpOnly; Secure; SameSite=Lax; Max-Age=600", - state, - ); + let state_cookie = oidc_state_cookie(&state, 600); Redirect::new(redirect_url) .into_response()? @@ -692,18 +720,18 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot: let lang = detect_lang(&request); let fail = |code: &str| format!("/admin/login?lang={}&error={}", lang.code(), code); - // Read saved state from cookie - let saved_state = request - .headers() - .get("cookie") - .and_then(|v| v.to_str().ok()) - .and_then(|cookies| { - cookies.split(';').find_map(|part| { - let part = part.trim(); - part.strip_prefix("oidc_state=").map(|v| v.to_string()) - }) - }) - .unwrap_or_default(); + // Prefer the server-side session; keep the cookie as a compatibility + // fallback for flows started before this code was deployed. + let saved_state_from_session = session + .get::(SESSION_OIDC_STATE) + .await + .ok() + .flatten(); + let saved_state_from_cookie = get_cookie(&request, "oidc_state"); + let saved_state = saved_state_from_session + .as_deref() + .or(saved_state_from_cookie.as_deref()) + .unwrap_or(""); // Extract code and state from query string let query_str = request.uri().query().unwrap_or(""); @@ -719,12 +747,20 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot: if code.is_empty() || state.is_empty() || state != saved_state { tracing::warn!( - "OIDC state mismatch: state={state:?}, saved={saved_state:?}, code_empty={}, state_empty={}", - code.is_empty(), - state.is_empty(), + target: "oidc", + has_session_state = saved_state_from_session.is_some(), + has_cookie_state = saved_state_from_cookie.is_some(), + code_empty = code.is_empty(), + state_empty = state.is_empty(), + "OIDC state mismatch", ); - return Redirect::new(fail("sso")).into_response(); + let clear_cookie = oidc_state_cookie("", 0); + return Redirect::new(fail("sso")) + .into_response()? + .with_header("set-cookie", clear_cookie) + .into_response(); } + let _ = session.remove::(SESSION_OIDC_STATE).await; let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?; let client_id = oidc_setting(&db, "oidc_client_id").await?; @@ -886,7 +922,7 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot: session.insert(SESSION_USER_NAME, session_name).await?; // Clear the oidc_state cookie - let clear_cookie = "oidc_state=; Path=/admin/oidc; HttpOnly; Secure; SameSite=Lax; Max-Age=0"; + let clear_cookie = oidc_state_cookie("", 0); Redirect::new(format!("/admin/?lang={}", lang.code())) .into_response()? .with_header("set-cookie", clear_cookie)