use crate::{config, file_kind::FileKind}; use actix_multipart::{Field, Multipart}; use actix_web::{error, http::header::DispositionParam, Error}; use futures_util::{StreamExt, TryStreamExt}; use std::path::Path; use time::OffsetDateTime; use time::{ext::NumericalDuration, Duration}; use tokio::{fs::File, io::AsyncWriteExt}; const MAX_UPLOAD_SECONDS: i64 = 31 * 24 * 60 * 60; const DEFAULT_UPLOAD_SECONDS: u32 = 30 * 60; pub(crate) struct UploadConfig { pub original_name: String, pub valid_till: OffsetDateTime, pub kind: FileKind, pub delete_on_download: bool, } pub(crate) async fn parse_multipart( mut payload: Multipart, file_id: &str, file_name: &Path, config: &config::Config, ) -> Result { let mut original_name: Option = None; let mut keep_for: Option = None; let mut kind: Option = None; let mut delete_on_download = false; let mut password = None; let mut size = 0; while let Ok(Some(field)) = payload.try_next().await { let name = get_field_name(&field)?; let name = name.as_str(); match name { "keep_for" => { keep_for = Some(parse_string(name, field).await?); } "file" => { let file_original_name = get_original_filename(&field); if file_original_name == None || file_original_name.map(|f| f.as_str()) == Some("") { continue; } original_name = file_original_name.map(|f| f.to_string()); kind = Some(FileKind::Binary); size = create_file(file_name, field, config.max_file_size).await?; } "text" => { if original_name.is_some() { continue; } original_name = Some(format!("{}.txt", file_id)); kind = Some(FileKind::Text); size = create_file(file_name, field, config.max_file_size).await?; } "delete_on_download" => { delete_on_download = parse_string(name, field).await? != "false"; } "password" => { password = Some(parse_string(name, field).await?); } _ => {} }; } let original_name = original_name.ok_or_else(|| error::ErrorBadRequest("no content found"))?; let kind = kind.ok_or_else(|| error::ErrorBadRequest("no content found"))?; let keep_for: u32 = keep_for .map(|k| k.parse()) .transpose() .map_err(|e| error::ErrorBadRequest(format!("field keep_for is not a number: {}", e)))? .unwrap_or(DEFAULT_UPLOAD_SECONDS); let valid_duration = keep_for.seconds(); let valid_till = OffsetDateTime::now_utc() + valid_duration; let upload_config = UploadConfig { original_name, valid_till, kind, delete_on_download, }; check_requirements(&upload_config, size, password, &valid_duration, config)?; Ok(upload_config) } fn check_requirements( upload_config: &UploadConfig, size: u64, password: Option, valid_duration: &Duration, config: &config::Config, ) -> Result<(), error::Error> { if upload_config.original_name.len() > 255 { return Err(error::ErrorBadRequest("filename is too long")); } let valid_seconds = valid_duration.whole_seconds(); if valid_seconds > MAX_UPLOAD_SECONDS { return Err(error::ErrorBadRequest(format!( "maximum allowed validity is {} seconds, but you specified {} seconds", MAX_UPLOAD_SECONDS, valid_seconds ))); } if let Some(no_auth_limits) = &config.no_auth_limits { let requires_auth = valid_seconds > no_auth_limits.max_time.whole_seconds() || valid_seconds > no_auth_limits.large_file_max_time.whole_seconds() && size > no_auth_limits.large_file_size; // hIGh sECUriTy paSsWoRD CHEck if requires_auth && password.as_ref() != Some(&no_auth_limits.auth_password) { return Err(error::ErrorBadRequest( "upload requires authentication, but authentication was incorrect", )); } } Ok(()) } fn get_field_name(field: &Field) -> Result { Ok(field .content_disposition() .get_name() .map(|s| s.to_owned()) .ok_or(error::ParseError::Incomplete)?) } async fn parse_string(name: &str, field: actix_multipart::Field) -> Result { let data = read_content(field).await?; String::from_utf8(data) .map_err(|_| error::ErrorBadRequest(format!("could not parse field {} as utf-8", name))) } async fn read_content(mut field: actix_multipart::Field) -> Result, error::Error> { let mut data = Vec::new(); while let Some(chunk) = field.try_next().await.map_err(error::ErrorBadRequest)? { data.extend(chunk); } Ok(data) } async fn create_file( filename: &Path, field: Field, max_file_size: Option, ) -> Result { let mut file = File::create(&filename).await.map_err(|file_err| { log::error!("could not create file {:?}", file_err); error::ErrorInternalServerError("could not create file") })?; let written_bytes = write_to_file(&mut file, field, max_file_size).await?; Ok(written_bytes) } async fn write_to_file( file: &mut File, mut field: Field, max_size: Option, ) -> Result { let mut written_bytes: u64 = 0; while let Some(chunk) = field.next().await { let chunk = chunk.map_err(error::ErrorBadRequest)?; written_bytes += chunk.len() as u64; if let Some(max_size) = max_size { if written_bytes > max_size { return Err(error::ErrorBadRequest(format!( "exceeded maximum file size of {} bytes", max_size ))); } } file.write_all(&chunk).await.map_err(|write_err| { log::error!("could not write file {:?}", write_err); error::ErrorInternalServerError("could not write file") })?; } Ok(written_bytes) } fn get_original_filename(field: &actix_multipart::Field) -> Option<&String> { field .content_disposition() .parameters .iter() .find_map(|param| match param { DispositionParam::Filename(filename) => Some(filename), _ => None, }) }