From 426c695b479496df2e48457da2882dd63b74d3a4 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Fri, 11 Sep 2020 19:32:10 -0400 Subject: [PATCH] File upload working --- Cargo.lock | 35 +++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/files.rs | 32 ++++++++++++++++++++++++++++++ src/main.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 4cce4a4..542e849 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,24 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-multipart" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774bfeb11b54bf9c857a005b8ab893293da4eaff79261a66a9200dab7f5ab6e3" +dependencies = [ + "actix-service", + "actix-utils 2.0.0", + "actix-web", + "bytes", + "derive_more", + "futures-util", + "httparse", + "log", + "mime", + "twoway", +] + [[package]] name = "actix-router" version = "0.2.4" @@ -1097,6 +1115,7 @@ name = "image-api" version = "0.1.0" dependencies = [ "actix-files", + "actix-multipart", "actix-rt", "actix-web", "bcrypt", @@ -2202,12 +2221,28 @@ dependencies = [ "trust-dns-proto", ] +[[package]] +name = "twoway" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc" +dependencies = [ + "memchr", + "unchecked-index", +] + [[package]] name = "typenum" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + [[package]] name = "unicase" version = "2.6.0" diff --git a/Cargo.toml b/Cargo.toml index 48a344f..1c1ab29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" actix-web = "3.0" actix-rt = "1.0" actix-files = "0.3.0" +actix-multipart = "0.3.0" futures = "0.3.5" jsonwebtoken = "7.2.0" serde = "1.0" diff --git a/src/files.rs b/src/files.rs index 5db361b..0261150 100644 --- a/src/files.rs +++ b/src/files.rs @@ -58,6 +58,17 @@ fn is_valid_full_path(base: &Path, path: &str) -> Option { } }) .ok() + } else if let Ok(path) = path.canonicalize().and_then(|path| { + if path.starts_with(base) { + Ok(path) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "Path below base directory", + )) + } + }) { + Some(path) } else { None } @@ -102,6 +113,27 @@ mod tests { ); } + #[test] + fn build_from_absolute_path_test() { + let base = env::temp_dir(); + let mut test_file = PathBuf::from(&base); + test_file.push("test.png"); + File::create(&test_file).unwrap(); + + assert!(is_valid_full_path(&base, test_file.to_str().unwrap()).is_some()); + + let path = "relative/path/test.png"; + let mut test_file = PathBuf::from(&base); + test_file.push(path); + create_dir_all(test_file.parent().unwrap()).unwrap(); + File::create(test_file).unwrap(); + + assert_eq!( + Some(PathBuf::from("/tmp/relative/path/test.png")), + is_valid_full_path(&base, path) + ); + } + #[test] fn png_valid_extension_test() { assert!(is_image_or_video(Path::new("image.png"))); diff --git a/src/main.rs b/src/main.rs index b89c240..73e933a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,13 +3,17 @@ extern crate diesel; extern crate rayon; use actix_files::NamedFile; +use actix_multipart as mp; use actix_web::web::{HttpRequest, HttpResponse, Json}; use actix_web::{get, post, web, App, HttpServer, Responder}; use chrono::{Duration, Utc}; use data::{AddFavoriteRequest, LoginRequest, ThumbnailRequest}; +use futures::stream::StreamExt; use jsonwebtoken::{encode, EncodingKey, Header}; use rayon::prelude::*; use serde::Serialize; +use std::fs::File; +use std::io::prelude::*; use std::path::{Path, PathBuf}; use crate::data::{secret_key, Claims, CreateAccountRequest, Token}; @@ -119,6 +123,56 @@ async fn get_image( } } +#[post("/image")] +async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder { + let mut file_content: Vec<_> = Vec::new(); + let mut file_name: Option = None; + let mut file_path: Option = None; + + while let Some(Ok(mut part)) = payload.next().await { + if let Some(content_type) = part.content_disposition() { + println!("{:?}", content_type); + if let Some(filename) = content_type.get_filename() { + println!("Name: {:?}", filename); + file_name = Some(filename.to_string()); + + while let Some(Ok(data)) = part.next().await { + file_content.extend_from_slice(data.as_ref()); + } + } else if content_type.get_name().map_or(false, |name| name == "path") { + while let Some(Ok(data)) = part.next().await { + if let Ok(path) = std::str::from_utf8(&data) { + file_path = Some(path.to_string()) + } + } + } + } + } + + let path = file_path.unwrap_or_else(|| dotenv::var("BASE_PATH").unwrap()); + if !file_content.is_empty() { + let full_path = PathBuf::from(&path); + + if let Some(mut full_path) = is_valid_path(full_path.to_str().unwrap_or("")) { + // TODO: Validate this file_name as is subject to path traversals which could lead to + // writing outside the base dir. + full_path = full_path.join(file_name.unwrap()); + + if !full_path.is_file() { + let mut file = File::create(full_path).unwrap(); + file.write_all(&file_content).unwrap(); + } else { + return HttpResponse::BadRequest().body("File already exists"); + } + } else { + return HttpResponse::BadRequest().body("Path was not valid"); + } + } else { + return HttpResponse::BadRequest().body("No file body read"); + } + HttpResponse::Ok().finish() +} + #[post("/video/generate")] async fn generate_video(_claims: Claims, body: web::Json) -> impl Responder { let filename = PathBuf::from(&body.path); @@ -257,6 +311,7 @@ async fn main() -> std::io::Result<()> { .service(login) .service(list_photos) .service(get_image) + .service(upload_image) .service(generate_video) .service(stream_video) .service(get_video_part)