use std::{io::Read, path::Path}; use chrono::{DateTime, Duration, Utc}; use multipart::server::{save::SavedData, Multipart, SaveResult}; use rocket::{http::ContentType, Data}; use rocket_contrib::json::Json; use serde_json::Value; use crate::{ api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, auth::{Headers, Host}, db::{models::*, DbConn}, CONFIG, }; pub fn routes() -> Vec { routes![ post_send, post_send_file, post_access, post_access_file, put_send, delete_send, put_remove_password ] } #[derive(Deserialize)] #[allow(non_snake_case)] pub struct SendData { pub Type: i32, pub Key: String, pub Password: Option, pub MaxAccessCount: Option, pub ExpirationDate: Option>, pub DeletionDate: DateTime, pub Disabled: bool, // Data field pub Name: String, pub Notes: Option, pub Text: Option, pub File: Option, } fn create_send(data: SendData, user_uuid: String) -> ApiResult { let data_val = if data.Type == SendType::Text as i32 { data.Text } else if data.Type == SendType::File as i32 { data.File } else { err!("Invalid Send type") }; let data_str = if let Some(mut d) = data_val { d.as_object_mut().and_then(|o| o.remove("Response")); serde_json::to_string(&d)? } else { err!("Send data not provided"); }; if data.DeletionDate > Utc::now() + Duration::days(31) { err!( "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." ); } let mut send = Send::new(data.Type, data.Name, data_str, data.Key, data.DeletionDate.naive_utc()); send.user_uuid = Some(user_uuid); send.notes = data.Notes; send.max_access_count = data.MaxAccessCount; send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); send.disabled = data.Disabled; send.atype = data.Type; send.set_password(data.Password.as_deref()); Ok(send) } #[post("/sends", data = "")] fn post_send(data: JsonUpcase, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { let data: SendData = data.into_inner().data; if data.Type == SendType::File as i32 { err!("File sends should use /api/sends/file") } let mut send = create_send(data, headers.user.uuid.clone())?; send.save(&conn)?; nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); Ok(Json(send.to_json())) } #[post("/sends/file", format = "multipart/form-data", data = "")] fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { let boundary = content_type.params().next().expect("No boundary provided").1; let mut mpart = Multipart::with_body(data.open(), boundary); // First entry is the SendData JSON let mut model_entry = match mpart.read_entry()? { Some(e) if &*e.headers.name == "model" => e, Some(_) => err!("Invalid entry name"), None => err!("No model entry present"), }; let mut buf = String::new(); model_entry.data.read_to_string(&mut buf)?; let data = serde_json::from_str::>(&buf)?; // Get the file length and add an extra 10% to avoid issues const SIZE_110_MB: u64 = 115_343_360; let size_limit = match CONFIG.user_attachment_limit() { Some(0) => err!("File uploads are disabled"), Some(limit_kb) => { let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn); if left <= 0 { err!("Attachment size limit reached! Delete some files to open space") } std::cmp::Ord::max(left as u64, SIZE_110_MB) } None => SIZE_110_MB, }; // Create the Send let mut send = create_send(data.data, headers.user.uuid.clone())?; let file_id: String = data_encoding::HEXLOWER.encode(&crate::crypto::get_random(vec![0; 32])); if send.atype != SendType::File as i32 { err!("Send content is not a file"); } let file_path = Path::new(&CONFIG.sends_folder()).join(&send.uuid).join(&file_id); // Read the data entry and save the file let mut data_entry = match mpart.read_entry()? { Some(e) if &*e.headers.name == "data" => e, Some(_) => err!("Invalid entry name"), None => err!("No model entry present"), }; let size = match data_entry .data .save() .memory_threshold(0) .size_limit(size_limit) .with_path(&file_path) { SaveResult::Full(SavedData::File(_, size)) => size as i32, SaveResult::Full(other) => { std::fs::remove_file(&file_path).ok(); err!(format!("Attachment is not a file: {:?}", other)); } SaveResult::Partial(_, reason) => { std::fs::remove_file(&file_path).ok(); err!(format!("Attachment size limit exceeded with this file: {:?}", reason)); } SaveResult::Error(e) => { std::fs::remove_file(&file_path).ok(); err!(format!("Error: {:?}", e)); } }; // Set ID and sizes let mut data_value: Value = serde_json::from_str(&send.data)?; if let Some(o) = data_value.as_object_mut() { o.insert(String::from("Id"), Value::String(file_id)); o.insert(String::from("Size"), Value::Number(size.into())); o.insert( String::from("SizeName"), Value::String(crate::util::get_display_size(size)), ); } send.data = serde_json::to_string(&data_value)?; // Save the changes in the database send.save(&conn)?; nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); Ok(Json(send.to_json())) } #[derive(Deserialize)] #[allow(non_snake_case)] pub struct SendAccessData { pub Password: Option, } #[post("/sends/access/", data = "")] fn post_access(access_id: String, data: JsonUpcase, conn: DbConn) -> JsonResult { let mut send = match Send::find_by_access_id(&access_id, &conn) { Some(s) => s, None => err_code!("Send not found", 404), }; if let Some(max_access_count) = send.max_access_count { if send.access_count >= max_access_count { err_code!("Max access count reached", 404); } } if let Some(expiration) = send.expiration_date { if Utc::now().naive_utc() >= expiration { err_code!("Send has expired", 404) } } if Utc::now().naive_utc() >= send.deletion_date { err_code!("Send has been deleted", 404) } if send.disabled { err_code!("Send has been disabled", 404) } if send.password_hash.is_some() { match data.into_inner().data.Password { Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } Some(_) => err!("Invalid password."), None => err_code!("Password not provided", 401), } } // Files are incremented during the download if send.atype == SendType::Text as i32 { send.access_count += 1; } send.save(&conn)?; Ok(Json(send.to_json())) } #[post("/sends//access/file/", data = "")] fn post_access_file( send_id: String, file_id: String, data: JsonUpcase, host: Host, conn: DbConn, ) -> JsonResult { let mut send = match Send::find_by_uuid(&send_id, &conn) { Some(s) => s, None => err_code!("Send not found", 404), }; if let Some(max_access_count) = send.max_access_count { if send.access_count >= max_access_count { err_code!("Max access count reached", 404); } } if let Some(expiration) = send.expiration_date { if Utc::now().naive_utc() >= expiration { err_code!("Send has expired", 404) } } if Utc::now().naive_utc() >= send.deletion_date { err_code!("Send has been deleted", 404) } if send.disabled { err_code!("Send has been disabled", 404) } if send.password_hash.is_some() { match data.into_inner().data.Password { Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } Some(_) => err!("Invalid password."), None => err_code!("Password not provided", 401), } } send.access_count += 1; send.save(&conn)?; Ok(Json(json!({ "Object": "send-fileDownload", "Id": file_id, "Url": format!("{}/sends/{}/{}", &host.host, send_id, file_id) }))) } #[put("/sends/", data = "")] fn put_send(id: String, data: JsonUpcase, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { let data: SendData = data.into_inner().data; let mut send = match Send::find_by_uuid(&id, &conn) { Some(s) => s, None => err!("Send not found"), }; if send.user_uuid.as_ref() != Some(&headers.user.uuid) { err!("Send is not owned by user") } if send.atype != data.Type { err!("Sends can't change type") } let data_val = if data.Type == SendType::Text as i32 { data.Text } else if data.Type == SendType::File as i32 { data.File } else { err!("Invalid Send type") }; let data_str = if let Some(mut d) = data_val { d.as_object_mut().and_then(|d| d.remove("Response")); serde_json::to_string(&d)? } else { err!("Send data not provided"); }; if data.DeletionDate > Utc::now() + Duration::days(31) { err!( "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." ); } send.data = data_str; send.name = data.Name; send.akey = data.Key; send.deletion_date = data.DeletionDate.naive_utc(); send.notes = data.Notes; send.max_access_count = data.MaxAccessCount; send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); send.disabled = data.Disabled; // Only change the value if it's present if let Some(password) = data.Password { send.set_password(Some(&password)); } send.save(&conn)?; nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); Ok(Json(send.to_json())) } #[delete("/sends/")] fn delete_send(id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { let send = match Send::find_by_uuid(&id, &conn) { Some(s) => s, None => err!("Send not found"), }; if send.user_uuid.as_ref() != Some(&headers.user.uuid) { err!("Send is not owned by user") } if send.atype == SendType::File as i32 { std::fs::remove_dir_all(Path::new(&CONFIG.sends_folder()).join(&send.uuid)).ok(); } send.delete(&conn)?; nt.send_user_update(UpdateType::SyncSendDelete, &headers.user); Ok(()) } #[put("/sends//remove-password")] fn put_remove_password(id: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { let mut send = match Send::find_by_uuid(&id, &conn) { Some(s) => s, None => err!("Send not found"), }; if send.user_uuid.as_ref() != Some(&headers.user.uuid) { err!("Send is not owned by user") } send.set_password(None); send.save(&conn)?; nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); Ok(Json(send.to_json())) }