mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-03-26 05:18:30 -06:00
Adjust checks for max access count, expiration date, and deletion date. The date checks aren't that important, but the access count check currently allows one more access than it should.
384 lines
12 KiB
Rust
384 lines
12 KiB
Rust
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<rocket::Route> {
|
|
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<String>,
|
|
pub MaxAccessCount: Option<i32>,
|
|
pub ExpirationDate: Option<DateTime<Utc>>,
|
|
pub DeletionDate: DateTime<Utc>,
|
|
pub Disabled: bool,
|
|
|
|
// Data field
|
|
pub Name: String,
|
|
pub Notes: Option<String>,
|
|
pub Text: Option<Value>,
|
|
pub File: Option<Value>,
|
|
}
|
|
|
|
fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> {
|
|
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 = "<data>")]
|
|
fn post_send(data: JsonUpcase<SendData>, 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 = "<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::<crate::util::UpCase<SendData>>(&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<String>,
|
|
}
|
|
|
|
#[post("/sends/access/<access_id>", data = "<data>")]
|
|
fn post_access(access_id: String, data: JsonUpcase<SendAccessData>, 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/<send_id>/access/file/<file_id>", data = "<data>")]
|
|
fn post_access_file(
|
|
send_id: String,
|
|
file_id: String,
|
|
data: JsonUpcase<SendAccessData>,
|
|
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/<id>", data = "<data>")]
|
|
fn put_send(id: String, data: JsonUpcase<SendData>, 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.key = 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/<id>")]
|
|
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/<id>/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()))
|
|
}
|