fix bugs, harden server, strip-based dithering, update docs

Firmware:
- fix 90° rotation (was transpose)
- fix Adafruit_Thermal constructor (spurious DTR pin arg)
- wire up uploadImage; heatInterval now applied
- PSRAM-free strip dithering: 2-row buffers (~1KB) replace 153KB PSRAM alloc
- consolidate all pins into config.h; BUTTON_PIN → Config::PIN_BUTTON
- constrain contrast/brightness/heat in settings save handler
- uploadImage size param int → size_t

Server:
- canonicalize upload_dir at startup (fixes path traversal guard)
- path traversal guard in serve_image
- replace unwrap in gallery_data with error handling
- IMAGE_WIDTH/IMAGE_HEIGHT named constants

Gallery:
- innerHTML → createElement (XSS-safe)
- encodeURIComponent on image URLs
- replace("_"," ") → regex /_/g

Docs: rewrite README, clarify GUEST_MANUAL placeholders
This commit is contained in:
2026-06-18 11:23:05 +02:00
parent 2f827c168d
commit 4ec723ef40
12 changed files with 263 additions and 219 deletions

View File

@@ -79,19 +79,32 @@
}
function addCard(filename) {
const div = document.createElement("div");
div.className = "card";
div.innerHTML = `
<a href="/gallery/img/${filename}" target="_blank">
<img src="/gallery/img/${filename}" loading="lazy" alt="Party Photo">
</a>
<div class="meta">${filename
.replace("capture_", "")
.replace(".png", "")
.replace("_", " ")}</div>
`;
// Prepend to show new ones at top
gallery.prepend(div);
const url = `/gallery/img/${encodeURIComponent(filename)}`;
const label = filename
.replace("capture_", "")
.replace(".png", "")
.replace(/_/g, " ");
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
const img = document.createElement("img");
img.src = url;
img.loading = "lazy";
img.alt = "Party Photo";
a.appendChild(img);
const meta = document.createElement("div");
meta.className = "meta";
meta.textContent = label;
const card = document.createElement("div");
card.className = "card";
card.appendChild(a);
card.appendChild(meta);
gallery.prepend(card);
}
// Initial load and poll every 5 seconds

View File

@@ -34,6 +34,7 @@ async fn main() -> anyhow::Result<()> {
PathBuf::from(env::var("UPLOAD_DIR").unwrap_or_else(|_| "./uploads".to_string()));
tokio::fs::create_dir_all(&upload_dir).await?;
let upload_dir = upload_dir.canonicalize()?;
let state = Arc::new(AppState {
upload_dir,
password,
@@ -89,7 +90,13 @@ async fn gallery_view() -> Html<&'static str> {
}
async fn gallery_data(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let mut entries = tokio::fs::read_dir(&state.upload_dir).await.unwrap();
let mut entries = match tokio::fs::read_dir(&state.upload_dir).await {
Ok(e) => e,
Err(e) => {
tracing::error!("Failed to read upload dir: {}", e);
return axum::Json(Vec::<String>::new()).into_response();
}
};
let mut images = Vec::new();
while let Ok(Some(entry)) = entries.next_entry().await {
@@ -104,7 +111,7 @@ async fn gallery_data(State(state): State<Arc<AppState>>) -> impl IntoResponse {
}
images.sort_by(|a, b| b.cmp(a));
axum::Json(images)
axum::Json(images).into_response()
}
async fn serve_image(
@@ -112,7 +119,13 @@ async fn serve_image(
Path(filename): Path<String>,
) -> impl IntoResponse {
let path = state.upload_dir.join(&filename);
match tokio::fs::read(&path).await {
let Ok(canonical) = path.canonicalize() else {
return StatusCode::NOT_FOUND.into_response();
};
if !canonical.starts_with(&state.upload_dir) {
return StatusCode::BAD_REQUEST.into_response();
}
match tokio::fs::read(&canonical).await {
Ok(bytes) => ([(header::CONTENT_TYPE, "image/png")], bytes).into_response(),
Err(_) => StatusCode::NOT_FOUND.into_response(),
}
@@ -150,10 +163,13 @@ async fn basic_auth(
.into_response()
}
// Must match FRAMESIZE_QVGA rotated 90°: camera captures 320×240, firmware rotates to 240×320.
const IMAGE_WIDTH: u32 = 240;
const IMAGE_HEIGHT: u32 = 320;
fn convert_and_save(data: &[u8], path: &PathBuf) -> anyhow::Result<()> {
// Dimensions from ESP32 code (240x320)
let width = 240;
let height = 320;
let width = IMAGE_WIDTH;
let height = IMAGE_HEIGHT;
let mut img = GrayImage::new(width, height);