234 lines
7.7 KiB
Rust
234 lines
7.7 KiB
Rust
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<String>,
|
|
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<UploadConfig, error::Error> {
|
|
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<UploadConfig, error::Error> {
|
|
let mut original_name: Option<String> = None;
|
|
let mut content_type: Option<Mime> = None;
|
|
let mut keep_for_seconds: Option<String> = 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<String>,
|
|
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<String, error::Error> {
|
|
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<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, Vec<u8>), 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<u64>,
|
|
) -> Result<(u64, Vec<u8>), 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<u64>) -> 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<Mime>, Option<String>) {
|
|
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<Mime> {
|
|
tree_magic_mini::from_u8(bytes).parse().ok()
|
|
}
|