Fixed image transcoding. Paying attention to EXIF orientation data
Build and Publish / Build and Publish Docker Image (push) Successful in 1m24s

This commit is contained in:
Ultradesu
2026-06-04 13:08:34 +03:00
parent 520960d009
commit 2389bca42b
3 changed files with 62 additions and 26 deletions
Generated
+1 -1
View File
@@ -3467,7 +3467,7 @@ dependencies = [
[[package]] [[package]]
name = "web-petting" name = "web-petting"
version = "0.1.12" version = "0.1.13"
dependencies = [ dependencies = [
"base64", "base64",
"chrono", "chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "web-petting" name = "web-petting"
version = "0.1.13" version = "0.1.14"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+60 -24
View File
@@ -6,6 +6,7 @@ use cot::request::extractors::Path;
use cot::response::{IntoResponse, Redirect, Response}; use cot::response::{IntoResponse, Redirect, Response};
use cot::router::{Route, Router}; use cot::router::{Route, Router};
use cot::session::Session; use cot::session::Session;
use image::ImageDecoder;
use image::ImageFormat; use image::ImageFormat;
use image::ImageReader; use image::ImageReader;
use image::codecs::jpeg::JpegEncoder; use image::codecs::jpeg::JpegEncoder;
@@ -19,6 +20,7 @@ use crate::telegram;
const SESSION_USER_ID: &str = "user_id"; const SESSION_USER_ID: &str = "user_id";
const SESSION_USER_NAME: &str = "user_name"; const SESSION_USER_NAME: &str = "user_name";
const SESSION_OIDC_STATE: &str = "oidc_state";
const MAX_UPLOADED_IMAGE_DIMENSION: u32 = 1920; const MAX_UPLOADED_IMAGE_DIMENSION: u32 = 1920;
const UPLOADED_IMAGE_JPEG_QUALITY: u8 = 82; const UPLOADED_IMAGE_JPEG_QUALITY: u8 = 82;
@@ -117,9 +119,16 @@ fn transcode_uploaded_image(data: &[u8], ext: &str) -> cot::Result<Option<Vec<u8
return Ok(None); return Ok(None);
}; };
let image = ImageReader::with_format(Cursor::new(data), format) let mut decoder = ImageReader::with_format(Cursor::new(data), format)
.decode() .into_decoder()
.map_err(|e| cot::Error::internal(e.to_string()))?; .map_err(|e| cot::Error::internal(e.to_string()))?;
let orientation = decoder
.orientation()
.map_err(|e| cot::Error::internal(e.to_string()))?;
let mut image = image::DynamicImage::from_decoder(decoder)
.map_err(|e| cot::Error::internal(e.to_string()))?;
image.apply_orientation(orientation);
let resized = image.resize( let resized = image.resize(
MAX_UPLOADED_IMAGE_DIMENSION, MAX_UPLOADED_IMAGE_DIMENSION,
MAX_UPLOADED_IMAGE_DIMENSION, MAX_UPLOADED_IMAGE_DIMENSION,
@@ -640,7 +649,28 @@ fn decode_jwt_payload(token: &str) -> Option<serde_json::Value> {
serde_json::from_slice(&bytes).ok() serde_json::from_slice(&bytes).ok()
} }
async fn oidc_start(request: Request, db: Database) -> cot::Result<Response> { 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<String> {
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<Response> {
let lang = detect_lang(&request); let lang = detect_lang(&request);
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?; let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
let client_id = oidc_setting(&db, "oidc_client_id").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<Response> {
}; };
let state = rand_token(); let state = rand_token();
session.insert(SESSION_OIDC_STATE, state.clone()).await?;
let redirect_uri = format!("{}/admin/oidc/callback", site_domain.trim_end_matches('/')); 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<Response> {
urlencoding::encode(&state), urlencoding::encode(&state),
); );
let state_cookie = format!( let state_cookie = oidc_state_cookie(&state, 600);
"oidc_state={}; Path=/admin/oidc; HttpOnly; Secure; SameSite=Lax; Max-Age=600",
state,
);
Redirect::new(redirect_url) Redirect::new(redirect_url)
.into_response()? .into_response()?
@@ -692,18 +720,18 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
let lang = detect_lang(&request); let lang = detect_lang(&request);
let fail = |code: &str| format!("/admin/login?lang={}&error={}", lang.code(), code); let fail = |code: &str| format!("/admin/login?lang={}&error={}", lang.code(), code);
// Read saved state from cookie // Prefer the server-side session; keep the cookie as a compatibility
let saved_state = request // fallback for flows started before this code was deployed.
.headers() let saved_state_from_session = session
.get("cookie") .get::<String>(SESSION_OIDC_STATE)
.and_then(|v| v.to_str().ok()) .await
.and_then(|cookies| { .ok()
cookies.split(';').find_map(|part| { .flatten();
let part = part.trim(); let saved_state_from_cookie = get_cookie(&request, "oidc_state");
part.strip_prefix("oidc_state=").map(|v| v.to_string()) let saved_state = saved_state_from_session
}) .as_deref()
}) .or(saved_state_from_cookie.as_deref())
.unwrap_or_default(); .unwrap_or("");
// Extract code and state from query string // Extract code and state from query string
let query_str = request.uri().query().unwrap_or(""); 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 { if code.is_empty() || state.is_empty() || state != saved_state {
tracing::warn!( tracing::warn!(
"OIDC state mismatch: state={state:?}, saved={saved_state:?}, code_empty={}, state_empty={}", target: "oidc",
code.is_empty(), has_session_state = saved_state_from_session.is_some(),
state.is_empty(), 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::<String>(SESSION_OIDC_STATE).await;
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?; let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
let client_id = oidc_setting(&db, "oidc_client_id").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?; session.insert(SESSION_USER_NAME, session_name).await?;
// Clear the oidc_state cookie // 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())) Redirect::new(format!("/admin/?lang={}", lang.code()))
.into_response()? .into_response()?
.with_header("set-cookie", clear_cookie) .with_header("set-cookie", clear_cookie)