diff --git a/src/web.rs b/src/web.rs index 802cfb7..29e0bac 100644 --- a/src/web.rs +++ b/src/web.rs @@ -42,6 +42,7 @@ pub fn router(state: Arc) -> Router { .route("/", get(index)) .route("/submit", post(submit)) .route("/drawings/{filename}", get(serve_drawing)) + .route("/voice_notes/{filename}", get(serve_voice_note)) .layer(DefaultBodyLimit::max(2 * 1024 * 1024)) .with_state(state) } @@ -92,6 +93,33 @@ async fn serve_drawing( } } +async fn serve_voice_note( + State(state): State>, + AxumPath(filename): AxumPath, +) -> Response { + if !filename.ends_with(".webm") + || !filename[..filename.len() - 5] + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return StatusCode::NOT_FOUND.into_response(); + } + + let path = state.config.data_dir.join("voice_notes").join(&filename); + match std::fs::read(&path) { + Ok(bytes) => ( + StatusCode::OK, + [ + (header::CONTENT_TYPE, "audio/webm"), + (header::X_CONTENT_TYPE_OPTIONS, "nosniff"), + ], + bytes, + ) + .into_response(), + Err(_) => StatusCode::NOT_FOUND.into_response(), + } +} + async fn submit( State(state): State>, Form(form): Form, @@ -979,4 +1007,40 @@ mod tests { let content = std::fs::read_to_string(entries[0].as_ref().unwrap().path()).unwrap(); assert!(content.contains("voice_note = \"\"")); } + + #[tokio::test] + async fn test_serve_voice_note() { + let dir = tempfile::tempdir().unwrap(); + let config = test_config(dir.path()); + let (app, _rx) = test_app(config); + + let vn_dir = dir.path().join("voice_notes"); + std::fs::create_dir_all(&vn_dir).unwrap(); + let webm_bytes = b"\x1a\x45\xdf\xa3fake"; + std::fs::write(vn_dir.join("test123.webm"), webm_bytes).unwrap(); + + let (status, body) = get_path(&app, "/voice_notes/test123.webm").await; + assert_eq!(status, StatusCode::OK); + assert_eq!(body, webm_bytes); + } + + #[tokio::test] + async fn test_serve_voice_note_not_found() { + let dir = tempfile::tempdir().unwrap(); + let config = test_config(dir.path()); + let (app, _rx) = test_app(config); + + let (status, _) = get_path(&app, "/voice_notes/nonexistent.webm").await; + assert_eq!(status, StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn test_serve_voice_note_rejects_path_traversal() { + let dir = tempfile::tempdir().unwrap(); + let config = test_config(dir.path()); + let (app, _rx) = test_app(config); + + let (status, _) = get_path(&app, "/voice_notes/../entries/secret.txt").await; + assert_eq!(status, StatusCode::NOT_FOUND); + } }