|
|
|
@ -12,7 +12,7 @@ use serde_json::Value; |
|
|
|
use crate::{ |
|
|
|
CONFIG, |
|
|
|
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType}, |
|
|
|
auth::{ClientIp, Headers, Host, SendHeaders}, |
|
|
|
auth::{Headers, Host, SendHeaders}, |
|
|
|
config::PathType, |
|
|
|
db::{ |
|
|
|
DbConn, DbPool, |
|
|
|
@ -46,11 +46,8 @@ pub fn routes() -> Vec<rocket::Route> { |
|
|
|
get_sends, |
|
|
|
get_send, |
|
|
|
post_send, |
|
|
|
post_send_file, |
|
|
|
post_access, |
|
|
|
post_access_legacy, |
|
|
|
post_access_file, |
|
|
|
post_access_file_legacy, |
|
|
|
put_send, |
|
|
|
delete_send, |
|
|
|
put_remove_password, |
|
|
|
@ -73,7 +70,7 @@ pub async fn purge_sends(pool: DbPool) { |
|
|
|
#[serde(rename_all = "camelCase")] |
|
|
|
pub struct SendData { |
|
|
|
r#type: i32, |
|
|
|
key: String, |
|
|
|
pub key: String, |
|
|
|
password: Option<String>, |
|
|
|
max_access_count: Option<NumberOrString>, |
|
|
|
expiration_date: Option<DateTime<Utc>>, |
|
|
|
@ -201,7 +198,7 @@ async fn post_send(data: Json<SendData>, headers: Headers, conn: DbConn, nt: Not |
|
|
|
enforce_disable_hide_email_policy(&data, &headers, &conn).await?; |
|
|
|
|
|
|
|
if data.r#type == SendType::File as i32 { |
|
|
|
err!("File sends should use /api/sends/file") |
|
|
|
err!("File sends should use /api/sends/file/v2") |
|
|
|
} |
|
|
|
|
|
|
|
let mut send = create_send(data, headers.user.uuid)?; |
|
|
|
@ -218,93 +215,11 @@ async fn post_send(data: Json<SendData>, headers: Headers, conn: DbConn, nt: Not |
|
|
|
Ok(Json(send.to_json())) |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(FromForm)] |
|
|
|
struct UploadData<'f> { |
|
|
|
model: Json<SendData>, |
|
|
|
data: TempFile<'f>, |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(FromForm)] |
|
|
|
struct UploadDataV2<'f> { |
|
|
|
data: TempFile<'f>, |
|
|
|
} |
|
|
|
|
|
|
|
// @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2).
|
|
|
|
// This method still exists to support older clients, probably need to remove it sometime.
|
|
|
|
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167
|
|
|
|
// 2025: This endpoint doesn't seem to exists anymore in the latest version
|
|
|
|
// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs
|
|
|
|
#[post("/sends/file", format = "multipart/form-data", data = "<data>")] |
|
|
|
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { |
|
|
|
enforce_disable_send_policy(&headers, &conn).await?; |
|
|
|
|
|
|
|
let UploadData { |
|
|
|
model, |
|
|
|
data, |
|
|
|
} = data.into_inner(); |
|
|
|
let model = model.into_inner(); |
|
|
|
|
|
|
|
let Some(size) = data.len().to_i64() else { |
|
|
|
err!("Invalid send size"); |
|
|
|
}; |
|
|
|
if size < 0 { |
|
|
|
err!("Send size can't be negative") |
|
|
|
} |
|
|
|
|
|
|
|
enforce_disable_hide_email_policy(&model, &headers, &conn).await?; |
|
|
|
|
|
|
|
let size_limit = match CONFIG.user_send_limit() { |
|
|
|
Some(0) => err!("File uploads are disabled"), |
|
|
|
Some(limit_kb) => { |
|
|
|
let Some(already_used) = Send::size_by_user(&headers.user.uuid, &conn).await else { |
|
|
|
err!("Existing sends overflow") |
|
|
|
}; |
|
|
|
let Some(left) = limit_kb.checked_mul(1024).and_then(|l| l.checked_sub(already_used)) else { |
|
|
|
err!("Send size overflow"); |
|
|
|
}; |
|
|
|
if left <= 0 { |
|
|
|
err!("Send storage limit reached! Delete some sends to free up space") |
|
|
|
} |
|
|
|
i64::clamp(left, 0, SIZE_525_MB) |
|
|
|
} |
|
|
|
None => SIZE_525_MB, |
|
|
|
}; |
|
|
|
|
|
|
|
if size > size_limit { |
|
|
|
err!("Send storage limit exceeded with this file"); |
|
|
|
} |
|
|
|
|
|
|
|
let mut send = create_send(model, headers.user.uuid)?; |
|
|
|
if send.atype != SendType::File as i32 { |
|
|
|
err!("Send content is not a file"); |
|
|
|
} |
|
|
|
|
|
|
|
let file_id = crate::crypto::generate_send_file_id(); |
|
|
|
|
|
|
|
save_temp_file(&PathType::Sends, &format!("{}/{file_id}", send.uuid), data, true).await?; |
|
|
|
|
|
|
|
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).await?; |
|
|
|
nt.send_send_update( |
|
|
|
UpdateType::SyncSendCreate, |
|
|
|
&send, |
|
|
|
&send.update_users_revision(&conn).await, |
|
|
|
&headers.device, |
|
|
|
&conn, |
|
|
|
) |
|
|
|
.await; |
|
|
|
|
|
|
|
Ok(Json(send.to_json())) |
|
|
|
} |
|
|
|
|
|
|
|
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L165
|
|
|
|
#[post("/sends/file/v2", data = "<data>")] |
|
|
|
async fn post_send_file_v2(data: Json<SendData>, headers: Headers, conn: DbConn) -> JsonResult { |
|
|
|
@ -450,63 +365,16 @@ async fn post_send_file_v2_data( |
|
|
|
|
|
|
|
#[post("/sends/access")] |
|
|
|
async fn post_access(headers: SendHeaders, conn: DbConn, nt: Notify<'_>) -> JsonResult { |
|
|
|
let Some(send) = Send::find_by_uuid(&headers.send_id, &conn).await else { |
|
|
|
let Some(mut send) = Send::find_by_uuid(&headers.send_id, &conn).await else { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
}; |
|
|
|
if !send.is_accessible() { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
} |
|
|
|
process_access(send, conn, nt).await |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Deserialize)] |
|
|
|
#[serde(rename_all = "camelCase")] |
|
|
|
pub struct SendAccessData { |
|
|
|
pub password: Option<String>, |
|
|
|
} |
|
|
|
|
|
|
|
// Legacy since web-2026.6.0
|
|
|
|
#[post("/sends/access/<access_id>", data = "<data>")] |
|
|
|
async fn post_access_legacy( |
|
|
|
access_id: &str, |
|
|
|
data: Json<SendAccessData>, |
|
|
|
conn: DbConn, |
|
|
|
ip: ClientIp, |
|
|
|
nt: Notify<'_>, |
|
|
|
) -> JsonResult { |
|
|
|
crate::ratelimit::check_limit_unauthenticated(&ip.ip)?; |
|
|
|
|
|
|
|
let Some(mut send) = Send::find_by_access_id(access_id, &conn).await else { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
}; |
|
|
|
|
|
|
|
if let Some(max_access_count) = send.max_access_count |
|
|
|
&& send.access_count >= max_access_count |
|
|
|
{ |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404); |
|
|
|
} |
|
|
|
|
|
|
|
if !send.is_accessible() { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
} |
|
|
|
|
|
|
|
if send.password_hash.is_some() { |
|
|
|
match data.into_inner().password { |
|
|
|
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } |
|
|
|
Some(_) => err!("Invalid password", format!("IP: {}.", ip.ip)), |
|
|
|
None => err_code!("Password not provided", format!("IP: {}.", ip.ip), 401), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Files are incremented during the download
|
|
|
|
if send.atype == SendType::Text as i32 { |
|
|
|
if !send.register_access(&conn).await? { |
|
|
|
if send.atype == SendType::Text as i32 && !send.register_access(&conn).await? { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
} |
|
|
|
} else { |
|
|
|
send.save(&conn).await?; |
|
|
|
} |
|
|
|
|
|
|
|
process_access(send, conn, nt).await |
|
|
|
} |
|
|
|
|
|
|
|
@ -531,55 +399,27 @@ async fn post_access_file( |
|
|
|
conn: DbConn, |
|
|
|
nt: Notify<'_>, |
|
|
|
) -> JsonResult { |
|
|
|
let Some(send) = Send::find_by_uuid(&headers.send_id, &conn).await else { |
|
|
|
let Some(mut send) = Send::find_by_uuid(&headers.send_id, &conn).await else { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
}; |
|
|
|
if !send.is_accessible() { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
} |
|
|
|
process_access_file(send, file_id, host, conn, nt).await |
|
|
|
} |
|
|
|
|
|
|
|
// Legacy since web-2026.6.0
|
|
|
|
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")] |
|
|
|
async fn post_access_file_legacy( |
|
|
|
send_id: SendId, |
|
|
|
file_id: SendFileId, |
|
|
|
data: Json<SendAccessData>, |
|
|
|
host: Host, |
|
|
|
conn: DbConn, |
|
|
|
ip: ClientIp, |
|
|
|
nt: Notify<'_>, |
|
|
|
) -> JsonResult { |
|
|
|
crate::ratelimit::check_limit_unauthenticated(&ip.ip)?; |
|
|
|
|
|
|
|
let Some(mut send) = Send::find_by_uuid(&send_id, &conn).await else { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
}; |
|
|
|
|
|
|
|
if let Some(max_access_count) = send.max_access_count |
|
|
|
&& send.access_count >= max_access_count |
|
|
|
{ |
|
|
|
check_send_file_id(&send, &file_id)?; |
|
|
|
if !send.register_access(&conn).await? { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
} |
|
|
|
|
|
|
|
if !send.is_accessible() { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
process_access_file(send, file_id, host, conn, nt).await |
|
|
|
} |
|
|
|
|
|
|
|
if send.password_hash.is_some() { |
|
|
|
match data.into_inner().password { |
|
|
|
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } |
|
|
|
Some(_) => err!("Invalid password."), |
|
|
|
None => err_code!("Password not provided", 401), |
|
|
|
} |
|
|
|
fn check_send_file_id(send: &Send, file_id: &SendFileId) -> EmptyResult { |
|
|
|
if send.atype != SendType::File as i32 { |
|
|
|
err!("Send is not a file type send."); |
|
|
|
} |
|
|
|
|
|
|
|
if !send.register_access(&conn).await? { |
|
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404) |
|
|
|
match serde_json::from_str::<SendFileData>(&send.data) { |
|
|
|
Ok(data) if &data.id == file_id => Ok(()), |
|
|
|
_ => err_code!(SEND_INACCESSIBLE_MSG, 404), |
|
|
|
} |
|
|
|
|
|
|
|
process_access_file(send, file_id, host, conn, nt).await |
|
|
|
} |
|
|
|
|
|
|
|
async fn process_access_file(send: Send, file_id: SendFileId, host: Host, conn: DbConn, nt: Notify<'_>) -> JsonResult { |
|
|
|
@ -642,7 +482,7 @@ async fn put_send(send_id: SendId, data: Json<SendData>, headers: Headers, conn: |
|
|
|
Ok(Json(send.to_json())) |
|
|
|
} |
|
|
|
|
|
|
|
pub async fn update_send_from_data( |
|
|
|
async fn update_send_from_data( |
|
|
|
send: &mut Send, |
|
|
|
data: SendData, |
|
|
|
headers: &Headers, |
|
|
|
|