Fixed image transcoding. Paying attention to EXIF orientation data
Build and Publish / Build and Publish Docker Image (push) Successful in 1m24s
Build and Publish / Build and Publish Docker Image (push) Successful in 1m24s
This commit is contained in:
Generated
+1
-1
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user