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]]
name = "web-petting"
version = "0.1.12"
version = "0.1.13"
dependencies = [
"base64",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "web-petting"
version = "0.1.13"
version = "0.1.14"
edition = "2024"
[dependencies]
+60 -24
View File
@@ -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<Vec<u8
return Ok(None);
};
let image = ImageReader::with_format(Cursor::new(data), format)
.decode()
let mut decoder = ImageReader::with_format(Cursor::new(data), format)
.into_decoder()
.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(
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()
}
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 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<Response> {
};
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<Response> {
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::<String>(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::<String>(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)