feature/exif-endpoint #44

Merged
cameron merged 29 commits from feature/exif-endpoint into master 2025-12-27 03:25:19 +00:00
Showing only changes of commit c6b1b46629 - Show all commits

View File

@@ -20,6 +20,7 @@ use std::{
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
use actix_files::NamedFile; use actix_files::NamedFile;
use actix_cors::Cors;
use actix_multipart as mp; use actix_multipart as mp;
use actix_web::{ use actix_web::{
App, HttpRequest, HttpResponse, HttpServer, Responder, delete, get, middleware, post, put, App, HttpRequest, HttpResponse, HttpServer, Responder, delete, get, middleware, post, put,
@@ -430,8 +431,34 @@ async fn get_video_part(
let mut file_part = PathBuf::new(); let mut file_part = PathBuf::new();
file_part.push(app_state.video_path.clone()); file_part.push(app_state.video_path.clone());
file_part.push(part); file_part.push(part);
// TODO: Do we need to guard against directory attacks here?
match NamedFile::open(&file_part) { // Guard against directory traversal attacks
let canonical_base = match std::fs::canonicalize(&app_state.video_path) {
Ok(path) => path,
Err(e) => {
error!("Failed to canonicalize video path: {:?}", e);
span.set_status(Status::error("Invalid video path configuration"));
return HttpResponse::InternalServerError().finish();
}
};
let canonical_file = match std::fs::canonicalize(&file_part) {
Ok(path) => path,
Err(_) => {
warn!("Video part not found or invalid: {:?}", file_part);
span.set_status(Status::error(format!("Video part not found '{}'", part)));
return HttpResponse::NotFound().finish();
}
};
// Ensure the resolved path is still within the video directory
if !canonical_file.starts_with(&canonical_base) {
warn!("Directory traversal attempt detected: {:?}", part);
span.set_status(Status::error("Invalid video path"));
return HttpResponse::Forbidden().finish();
}
match NamedFile::open(&canonical_file) {
Ok(file) => { Ok(file) => {
span.set_status(Status::Ok); span.set_status(Status::Ok);
file.into_response(&request) file.into_response(&request)
@@ -714,8 +741,30 @@ fn main() -> std::io::Result<()> {
let favorites_dao = SqliteFavoriteDao::new(); let favorites_dao = SqliteFavoriteDao::new();
let tag_dao = SqliteTagDao::default(); let tag_dao = SqliteTagDao::default();
let exif_dao = SqliteExifDao::new(); let exif_dao = SqliteExifDao::new();
let cors = Cors::default()
.allowed_origin_fn(|origin, _req_head| {
// Allow all origins in development, or check against CORS_ALLOWED_ORIGINS env var
if let Ok(allowed_origins) = env::var("CORS_ALLOWED_ORIGINS") {
allowed_origins.split(',').any(|allowed| {
origin.as_bytes() == allowed.trim().as_bytes()
})
} else {
// Default: allow all origins if not configured
true
}
})
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"])
.allowed_headers(vec![
actix_web::http::header::AUTHORIZATION,
actix_web::http::header::ACCEPT,
actix_web::http::header::CONTENT_TYPE,
])
.supports_credentials()
.max_age(3600);
App::new() App::new()
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.wrap(cors)
.service(web::resource("/login").route(web::post().to(login::<SqliteUserDao>))) .service(web::resource("/login").route(web::post().to(login::<SqliteUserDao>)))
.service( .service(
web::resource("/photos") web::resource("/photos")