use crate::{config, mime_relations}; use actix_multipart::{Field, Multipart}; use actix_web::{error, http::header::DispositionParam, Error}; use futures_util::{StreamExt, TryStreamExt}; use mime::{Mime, APPLICATION_OCTET_STREAM, TEXT_PLAIN}; use std::{cmp::min, io::ErrorKind, path::Path}; use time::{Duration, OffsetDateTime}; use tokio::{ fs::{self, File}, io::AsyncWriteExt, }; const MAX_UPLOAD_DURATION: Duration = Duration::days(31); const DEFAULT_UPLOAD_DURATION: Duration = Duration::minutes(30); pub(crate) struct UploadConfig { pub original_name: Option, pub content_type: Mime, pub valid_till: OffsetDateTime, pub delete_on_download: bool, } pub(crate) async fn parse_multipart( payload: Multipart, file_path: &Path, config: &config::Config, ) -> Result { match parse_multipart_inner(payload, file_path, config).await { Ok(data) => Ok(data), Err(err) => { match fs::remove_file(file_path).await { Err(err) if err.kind() != ErrorKind::NotFound => { log::error!("could not remove file {:?}", err); } _ => {} } Err(err) } } } pub(crate) async fn parse_multipart_inner( mut payload: Multipart, file_path: &Path, config: &config::Config, ) -> Result { let mut original_name: Option = None; let mut content_type: Option = None; let mut keep_for_seconds: Option = None; let mut delete_on_download = false; let mut password = None; let mut size = 0; while let Ok(Some(mut field)) = payload.try_next().await { let name = get_field_name(&field)?.to_owned(); match name.as_str() { "keep_for" => { keep_for_seconds = Some(parse_string(&name, &mut field).await?); } "file" => { let (mime, uploaded_name) = get_file_metadata(&field); if uploaded_name.is_none() || uploaded_name.as_deref() == Some("") { continue; } original_name = uploaded_name; let first_bytes; (size, first_bytes) = create_file(file_path, field, config.max_file_size).await?; content_type = Some( mime.filter(|mime| *mime != APPLICATION_OCTET_STREAM) .map(mime_relations::get_alias) .or_else(|| get_content_type(&first_bytes)) .unwrap_or(APPLICATION_OCTET_STREAM), ); } "text" => { if original_name.is_some() { continue; } let first_bytes; (size, first_bytes) = create_file(file_path, field, config.max_file_size).await?; content_type = Some(get_content_type(&first_bytes).unwrap_or(TEXT_PLAIN)); } "delete_on_download" => { delete_on_download = parse_string(&name, &mut field).await? != "false"; } "password" => { password = Some(parse_string(&name, &mut field).await?); } _ => {} }; } let content_type = content_type.ok_or_else(|| error::ErrorBadRequest("no content type found"))?; let keep_for = keep_for_seconds .map(|k| k.parse()) .transpose() .map_err(|e| error::ErrorBadRequest(format!("field keep_for is not a number: {e}")))? .map_or(DEFAULT_UPLOAD_DURATION, Duration::seconds); let valid_till = OffsetDateTime::now_utc() + keep_for; let upload_config = UploadConfig { original_name, content_type, valid_till, delete_on_download, }; check_requirements(&upload_config, size, &password, &keep_for, config)?; Ok(upload_config) } fn check_requirements( upload_config: &UploadConfig, size: u64, password: &Option, keep_for: &Duration, config: &config::Config, ) -> Result<(), error::Error> { if let Some(original_name) = upload_config.original_name.as_ref() { if original_name.len() > 255 { return Err(error::ErrorBadRequest("filename is too long")); } } if *keep_for > MAX_UPLOAD_DURATION { return Err(error::ErrorBadRequest(format!( "maximum allowed validity is {MAX_UPLOAD_DURATION}, but you specified {keep_for}" ))); } if let Some(no_auth_limits) = &config.no_auth_limits { let requires_auth = *keep_for > no_auth_limits.max_time || *keep_for > no_auth_limits.large_file_max_time && 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<&str, error::Error> { Ok(field .content_disposition() .get_name() .ok_or(error::ParseError::Incomplete)?) } async fn parse_string( name: &str, field: &mut actix_multipart::Field, ) -> Result { let data = read_content(field).await?; String::from_utf8(data) .map_err(|_| error::ErrorBadRequest(format!("could not parse field {name} as utf-8"))) } async fn read_content(field: &mut 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<(u64, Vec), Error> { 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") })?; write_to_file(&mut file, field, max_file_size).await } async fn write_to_file( file: &mut File, mut field: Field, max_size: Option, ) -> Result<(u64, Vec), error::Error> { let mut first_bytes = Vec::with_capacity(2048); 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; validate_max_size(written_bytes, max_size)?; let remaining_first_bytes = min(2048 - first_bytes.len(), chunk.len()); first_bytes.extend_from_slice(&chunk[..remaining_first_bytes]); 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, first_bytes)) } fn validate_max_size(written_bytes: u64, max_size: Option) -> Result<(), Error> { if let Some(max_size) = max_size { if written_bytes > max_size { return Err(error::ErrorPayloadTooLarge(format!( "exceeded maximum file size of {max_size} bytes" ))); } } Ok(()) } fn get_file_metadata(field: &actix_multipart::Field) -> (Option, Option) { let mime = field.content_type().cloned(); let filename = field .content_disposition() .parameters .iter() .find_map(|param| match param { DispositionParam::Filename(filename) => Some(filename.clone()), _ => None, }); (mime, filename) } fn get_content_type(bytes: &[u8]) -> Option { tree_magic_mini::from_u8(bytes).parse().ok() }