A website for temporary file- or text hosting
https://trash.ctdo.de/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
6.5 KiB
193 lines
6.5 KiB
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<UploadConfig, error::Error> { |
|
let mut original_name: Option<String> = None; |
|
let mut keep_for: Option<String> = None; |
|
let mut kind: Option<FileKind> = 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<String>, |
|
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<String, error::Error> { |
|
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<String, error::Error> { |
|
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<Vec<u8>, 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<u64>, |
|
) -> Result<u64, 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") |
|
})?; |
|
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<u64>, |
|
) -> Result<u64, error::Error> { |
|
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, |
|
}) |
|
}
|
|
|