mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-12 19:45:58 +03:00
Update to rocket 0.5 and made code async, missing updating all db calls, that are currently blocking
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use chrono::Utc;
|
||||
use rocket_contrib::json::Json;
|
||||
use rocket::serde::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
|
@@ -1,13 +1,14 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use rocket::{http::ContentType, request::Form, Data, Route};
|
||||
use rocket_contrib::json::Json;
|
||||
use rocket::fs::TempFile;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{
|
||||
form::{Form, FromForm},
|
||||
Route,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use multipart::server::{save::SavedData, Multipart, SaveResult};
|
||||
|
||||
use crate::{
|
||||
api::{self, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType},
|
||||
auth::Headers,
|
||||
@@ -79,9 +80,9 @@ pub fn routes() -> Vec<Route> {
|
||||
]
|
||||
}
|
||||
|
||||
pub fn purge_trashed_ciphers(pool: DbPool) {
|
||||
pub async fn purge_trashed_ciphers(pool: DbPool) {
|
||||
debug!("Purging trashed ciphers");
|
||||
if let Ok(conn) = pool.get() {
|
||||
if let Ok(conn) = pool.get().await {
|
||||
Cipher::purge_trash(&conn);
|
||||
} else {
|
||||
error!("Failed to get DB connection while purging trashed ciphers")
|
||||
@@ -90,12 +91,12 @@ pub fn purge_trashed_ciphers(pool: DbPool) {
|
||||
|
||||
#[derive(FromForm, Default)]
|
||||
struct SyncData {
|
||||
#[form(field = "excludeDomains")]
|
||||
#[field(name = "excludeDomains")]
|
||||
exclude_domains: bool, // Default: 'false'
|
||||
}
|
||||
|
||||
#[get("/sync?<data..>")]
|
||||
fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> Json<Value> {
|
||||
fn sync(data: SyncData, headers: Headers, conn: DbConn) -> Json<Value> {
|
||||
let user_json = headers.user.to_json(&conn);
|
||||
|
||||
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
|
||||
@@ -828,6 +829,12 @@ fn post_attachment_v2(
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct UploadData<'f> {
|
||||
key: Option<String>,
|
||||
data: TempFile<'f>,
|
||||
}
|
||||
|
||||
/// Saves the data content of an attachment to a file. This is common code
|
||||
/// shared between the v2 and legacy attachment APIs.
|
||||
///
|
||||
@@ -836,22 +843,21 @@ fn post_attachment_v2(
|
||||
///
|
||||
/// When used with the v2 API, post_attachment_v2() has already created the
|
||||
/// database record, which is passed in as `attachment`.
|
||||
fn save_attachment(
|
||||
async fn save_attachment(
|
||||
mut attachment: Option<Attachment>,
|
||||
cipher_uuid: String,
|
||||
data: Data,
|
||||
content_type: &ContentType,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: &Headers,
|
||||
conn: &DbConn,
|
||||
nt: Notify,
|
||||
) -> Result<Cipher, crate::error::Error> {
|
||||
let cipher = match Cipher::find_by_uuid(&cipher_uuid, conn) {
|
||||
conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> Result<(Cipher, DbConn), crate::error::Error> {
|
||||
let cipher = match Cipher::find_by_uuid(&cipher_uuid, &conn) {
|
||||
Some(cipher) => cipher,
|
||||
None => err_discard!("Cipher doesn't exist", data),
|
||||
None => err!("Cipher doesn't exist"),
|
||||
};
|
||||
|
||||
if !cipher.is_write_accessible_to_user(&headers.user.uuid, conn) {
|
||||
err_discard!("Cipher is not write accessible", data)
|
||||
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
|
||||
err!("Cipher is not write accessible")
|
||||
}
|
||||
|
||||
// In the v2 API, the attachment record has already been created,
|
||||
@@ -863,11 +869,11 @@ fn save_attachment(
|
||||
|
||||
let size_limit = if let Some(ref user_uuid) = cipher.user_uuid {
|
||||
match CONFIG.user_attachment_limit() {
|
||||
Some(0) => err_discard!("Attachments are disabled", data),
|
||||
Some(0) => err!("Attachments are disabled"),
|
||||
Some(limit_kb) => {
|
||||
let left = (limit_kb * 1024) - Attachment::size_by_user(user_uuid, conn) + size_adjust;
|
||||
let left = (limit_kb * 1024) - Attachment::size_by_user(user_uuid, &conn) + size_adjust;
|
||||
if left <= 0 {
|
||||
err_discard!("Attachment storage limit reached! Delete some attachments to free up space", data)
|
||||
err!("Attachment storage limit reached! Delete some attachments to free up space")
|
||||
}
|
||||
Some(left as u64)
|
||||
}
|
||||
@@ -875,130 +881,78 @@ fn save_attachment(
|
||||
}
|
||||
} else if let Some(ref org_uuid) = cipher.organization_uuid {
|
||||
match CONFIG.org_attachment_limit() {
|
||||
Some(0) => err_discard!("Attachments are disabled", data),
|
||||
Some(0) => err!("Attachments are disabled"),
|
||||
Some(limit_kb) => {
|
||||
let left = (limit_kb * 1024) - Attachment::size_by_org(org_uuid, conn) + size_adjust;
|
||||
let left = (limit_kb * 1024) - Attachment::size_by_org(org_uuid, &conn) + size_adjust;
|
||||
if left <= 0 {
|
||||
err_discard!("Attachment storage limit reached! Delete some attachments to free up space", data)
|
||||
err!("Attachment storage limit reached! Delete some attachments to free up space")
|
||||
}
|
||||
Some(left as u64)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
} else {
|
||||
err_discard!("Cipher is neither owned by a user nor an organization", data);
|
||||
err!("Cipher is neither owned by a user nor an organization");
|
||||
};
|
||||
|
||||
let mut params = content_type.params();
|
||||
let boundary_pair = params.next().expect("No boundary provided");
|
||||
let boundary = boundary_pair.1;
|
||||
let mut data = data.into_inner();
|
||||
|
||||
let base_path = Path::new(&CONFIG.attachments_folder()).join(&cipher_uuid);
|
||||
let mut path = PathBuf::new();
|
||||
|
||||
let mut attachment_key = None;
|
||||
let mut error = None;
|
||||
|
||||
Multipart::with_body(data.open(), boundary)
|
||||
.foreach_entry(|mut field| {
|
||||
match &*field.headers.name {
|
||||
"key" => {
|
||||
use std::io::Read;
|
||||
let mut key_buffer = String::new();
|
||||
if field.data.read_to_string(&mut key_buffer).is_ok() {
|
||||
attachment_key = Some(key_buffer);
|
||||
}
|
||||
}
|
||||
"data" => {
|
||||
// In the legacy API, this is the encrypted filename
|
||||
// provided by the client, stored to the database as-is.
|
||||
// In the v2 API, this value doesn't matter, as it was
|
||||
// already provided and stored via an earlier API call.
|
||||
let encrypted_filename = field.headers.filename;
|
||||
|
||||
// This random ID is used as the name of the file on disk.
|
||||
// In the legacy API, we need to generate this value here.
|
||||
// In the v2 API, we use the value from post_attachment_v2().
|
||||
let file_id = match &attachment {
|
||||
Some(attachment) => attachment.id.clone(), // v2 API
|
||||
None => crypto::generate_attachment_id(), // Legacy API
|
||||
};
|
||||
path = base_path.join(&file_id);
|
||||
|
||||
let size =
|
||||
match field.data.save().memory_threshold(0).size_limit(size_limit).with_path(path.clone()) {
|
||||
SaveResult::Full(SavedData::File(_, size)) => size as i32,
|
||||
SaveResult::Full(other) => {
|
||||
error = Some(format!("Attachment is not a file: {:?}", other));
|
||||
return;
|
||||
}
|
||||
SaveResult::Partial(_, reason) => {
|
||||
error = Some(format!("Attachment storage limit exceeded with this file: {:?}", reason));
|
||||
return;
|
||||
}
|
||||
SaveResult::Error(e) => {
|
||||
error = Some(format!("Error: {:?}", e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(attachment) = &mut attachment {
|
||||
// v2 API
|
||||
|
||||
// Check the actual size against the size initially provided by
|
||||
// the client. Upstream allows +/- 1 MiB deviation from this
|
||||
// size, but it's not clear when or why this is needed.
|
||||
const LEEWAY: i32 = 1024 * 1024; // 1 MiB
|
||||
let min_size = attachment.file_size - LEEWAY;
|
||||
let max_size = attachment.file_size + LEEWAY;
|
||||
|
||||
if min_size <= size && size <= max_size {
|
||||
if size != attachment.file_size {
|
||||
// Update the attachment with the actual file size.
|
||||
attachment.file_size = size;
|
||||
attachment.save(conn).expect("Error updating attachment");
|
||||
}
|
||||
} else {
|
||||
attachment.delete(conn).ok();
|
||||
|
||||
let err_msg = "Attachment size mismatch".to_string();
|
||||
error!("{} (expected within [{}, {}], got {})", err_msg, min_size, max_size, size);
|
||||
error = Some(err_msg);
|
||||
}
|
||||
} else {
|
||||
// Legacy API
|
||||
|
||||
if encrypted_filename.is_none() {
|
||||
error = Some("No filename provided".to_string());
|
||||
return;
|
||||
}
|
||||
if attachment_key.is_none() {
|
||||
error = Some("No attachment key provided".to_string());
|
||||
return;
|
||||
}
|
||||
let attachment = Attachment::new(
|
||||
file_id,
|
||||
cipher_uuid.clone(),
|
||||
encrypted_filename.unwrap(),
|
||||
size,
|
||||
attachment_key.clone(),
|
||||
);
|
||||
attachment.save(conn).expect("Error saving attachment");
|
||||
}
|
||||
}
|
||||
_ => error!("Invalid multipart name"),
|
||||
}
|
||||
})
|
||||
.expect("Error processing multipart data");
|
||||
|
||||
if let Some(ref e) = error {
|
||||
std::fs::remove_file(path).ok();
|
||||
err!(e);
|
||||
if let Some(size_limit) = size_limit {
|
||||
if data.data.len() > size_limit {
|
||||
err!("Attachment storage limit exceeded with this file");
|
||||
}
|
||||
}
|
||||
|
||||
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(conn));
|
||||
let file_id = match &attachment {
|
||||
Some(attachment) => attachment.id.clone(), // v2 API
|
||||
None => crypto::generate_attachment_id(), // Legacy API
|
||||
};
|
||||
|
||||
Ok(cipher)
|
||||
let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(&cipher_uuid);
|
||||
let file_path = folder_path.join(&file_id);
|
||||
tokio::fs::create_dir_all(&folder_path).await?;
|
||||
|
||||
let size = data.data.len() as i32;
|
||||
if let Some(attachment) = &mut attachment {
|
||||
// v2 API
|
||||
|
||||
// Check the actual size against the size initially provided by
|
||||
// the client. Upstream allows +/- 1 MiB deviation from this
|
||||
// size, but it's not clear when or why this is needed.
|
||||
const LEEWAY: i32 = 1024 * 1024; // 1 MiB
|
||||
let min_size = attachment.file_size - LEEWAY;
|
||||
let max_size = attachment.file_size + LEEWAY;
|
||||
|
||||
if min_size <= size && size <= max_size {
|
||||
if size != attachment.file_size {
|
||||
// Update the attachment with the actual file size.
|
||||
attachment.file_size = size;
|
||||
attachment.save(&conn).expect("Error updating attachment");
|
||||
}
|
||||
} else {
|
||||
attachment.delete(&conn).ok();
|
||||
|
||||
err!(format!("Attachment size mismatch (expected within [{}, {}], got {})", min_size, max_size, size));
|
||||
}
|
||||
} else {
|
||||
// Legacy API
|
||||
let encrypted_filename = data.data.raw_name().map(|s| s.dangerous_unsafe_unsanitized_raw().to_string());
|
||||
|
||||
if encrypted_filename.is_none() {
|
||||
err!("No filename provided")
|
||||
}
|
||||
if data.key.is_none() {
|
||||
err!("No attachment key provided")
|
||||
}
|
||||
let attachment = Attachment::new(file_id, cipher_uuid.clone(), encrypted_filename.unwrap(), size, data.key);
|
||||
attachment.save(&conn).expect("Error saving attachment");
|
||||
}
|
||||
|
||||
data.data.persist_to(file_path).await?;
|
||||
|
||||
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
|
||||
|
||||
Ok((cipher, conn))
|
||||
}
|
||||
|
||||
/// v2 API for uploading the actual data content of an attachment.
|
||||
@@ -1006,14 +960,13 @@ fn save_attachment(
|
||||
/// /ciphers/<uuid>/attachment/v2 route, which would otherwise conflict
|
||||
/// with this one.
|
||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>", format = "multipart/form-data", data = "<data>", rank = 1)]
|
||||
fn post_attachment_v2_data(
|
||||
async fn post_attachment_v2_data(
|
||||
uuid: String,
|
||||
attachment_id: String,
|
||||
data: Data,
|
||||
content_type: &ContentType,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
let attachment = match Attachment::find_by_id(&attachment_id, &conn) {
|
||||
Some(attachment) if uuid == attachment.cipher_uuid => Some(attachment),
|
||||
@@ -1021,54 +974,51 @@ fn post_attachment_v2_data(
|
||||
None => err!("Attachment doesn't exist"),
|
||||
};
|
||||
|
||||
save_attachment(attachment, uuid, data, content_type, &headers, &conn, nt)?;
|
||||
save_attachment(attachment, uuid, data, &headers, conn, nt).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Legacy API for creating an attachment associated with a cipher.
|
||||
#[post("/ciphers/<uuid>/attachment", format = "multipart/form-data", data = "<data>")]
|
||||
fn post_attachment(
|
||||
async fn post_attachment(
|
||||
uuid: String,
|
||||
data: Data,
|
||||
content_type: &ContentType,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
// Setting this as None signifies to save_attachment() that it should create
|
||||
// the attachment database record as well as saving the data to disk.
|
||||
let attachment = None;
|
||||
|
||||
let cipher = save_attachment(attachment, uuid, data, content_type, &headers, &conn, nt)?;
|
||||
let (cipher, conn) = save_attachment(attachment, uuid, data, &headers, conn, nt).await?;
|
||||
|
||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/attachment-admin", format = "multipart/form-data", data = "<data>")]
|
||||
fn post_attachment_admin(
|
||||
async fn post_attachment_admin(
|
||||
uuid: String,
|
||||
data: Data,
|
||||
content_type: &ContentType,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
post_attachment(uuid, data, content_type, headers, conn, nt)
|
||||
post_attachment(uuid, data, headers, conn, nt).await
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
|
||||
fn post_attachment_share(
|
||||
async fn post_attachment_share(
|
||||
uuid: String,
|
||||
attachment_id: String,
|
||||
data: Data,
|
||||
content_type: &ContentType,
|
||||
data: Form<UploadData<'_>>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn, &nt)?;
|
||||
post_attachment(uuid, data, content_type, headers, conn, nt)
|
||||
post_attachment(uuid, data, headers, conn, nt).await
|
||||
}
|
||||
|
||||
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
|
||||
@@ -1248,13 +1198,13 @@ fn move_cipher_selected_put(
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct OrganizationId {
|
||||
#[form(field = "organizationId")]
|
||||
#[field(name = "organizationId")]
|
||||
org_id: String,
|
||||
}
|
||||
|
||||
#[post("/ciphers/purge?<organization..>", data = "<data>")]
|
||||
fn delete_all(
|
||||
organization: Option<Form<OrganizationId>>,
|
||||
organization: Option<OrganizationId>,
|
||||
data: JsonUpcase<PasswordData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
use std::borrow::Borrow;
|
||||
|
||||
@@ -709,13 +709,13 @@ fn check_emergency_access_allowed() -> EmptyResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn emergency_request_timeout_job(pool: DbPool) {
|
||||
pub async fn emergency_request_timeout_job(pool: DbPool) {
|
||||
debug!("Start emergency_request_timeout_job");
|
||||
if !CONFIG.emergency_access_allowed() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(conn) = pool.get() {
|
||||
if let Ok(conn) = pool.get().await {
|
||||
let emergency_access_list = EmergencyAccess::find_all_recoveries(&conn);
|
||||
|
||||
if emergency_access_list.is_empty() {
|
||||
@@ -756,13 +756,13 @@ pub fn emergency_request_timeout_job(pool: DbPool) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emergency_notification_reminder_job(pool: DbPool) {
|
||||
pub async fn emergency_notification_reminder_job(pool: DbPool) {
|
||||
debug!("Start emergency_notification_reminder_job");
|
||||
if !CONFIG.emergency_access_allowed() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(conn) = pool.get() {
|
||||
if let Ok(conn) = pool.get().await {
|
||||
let emergency_access_list = EmergencyAccess::find_all_recoveries(&conn);
|
||||
|
||||
if emergency_access_list.is_empty() {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use rocket_contrib::json::Json;
|
||||
use rocket::serde::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
|
@@ -31,8 +31,8 @@ pub fn routes() -> Vec<Route> {
|
||||
//
|
||||
// Move this somewhere else
|
||||
//
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
@@ -144,7 +144,7 @@ fn put_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbC
|
||||
}
|
||||
|
||||
#[get("/hibp/breach?<username>")]
|
||||
fn hibp_breach(username: String) -> JsonResult {
|
||||
async fn hibp_breach(username: String) -> JsonResult {
|
||||
let url = format!(
|
||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{}?truncateResponse=false&includeUnverified=false",
|
||||
username
|
||||
@@ -153,14 +153,14 @@ fn hibp_breach(username: String) -> JsonResult {
|
||||
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
||||
let hibp_client = get_reqwest_client();
|
||||
|
||||
let res = hibp_client.get(&url).header("hibp-api-key", api_key).send()?;
|
||||
let res = hibp_client.get(&url).header("hibp-api-key", api_key).send().await?;
|
||||
|
||||
// If we get a 404, return a 404, it means no breached accounts
|
||||
if res.status() == 404 {
|
||||
return Err(Error::empty().with_code(404));
|
||||
}
|
||||
|
||||
let value: Value = res.error_for_status()?.json()?;
|
||||
let value: Value = res.error_for_status()?.json().await?;
|
||||
Ok(Json(value))
|
||||
} else {
|
||||
Ok(Json(json!([{
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use num_traits::FromPrimitive;
|
||||
use rocket::{request::Form, Route};
|
||||
use rocket_contrib::json::Json;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
@@ -469,12 +469,12 @@ fn put_collection_users(
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct OrgIdData {
|
||||
#[form(field = "organizationId")]
|
||||
#[field(name = "organizationId")]
|
||||
organization_id: String,
|
||||
}
|
||||
|
||||
#[get("/ciphers/organization-details?<data..>")]
|
||||
fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> Json<Value> {
|
||||
fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> Json<Value> {
|
||||
let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
|
||||
let ciphers_json: Vec<Value> =
|
||||
ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
|
||||
@@ -1097,14 +1097,14 @@ struct RelationsData {
|
||||
|
||||
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
|
||||
fn post_org_import(
|
||||
query: Form<OrgIdData>,
|
||||
query: OrgIdData,
|
||||
data: JsonUpcase<ImportData>,
|
||||
headers: AdminHeaders,
|
||||
conn: DbConn,
|
||||
nt: Notify,
|
||||
) -> EmptyResult {
|
||||
let data: ImportData = data.into_inner().data;
|
||||
let org_id = query.into_inner().organization_id;
|
||||
let org_id = query.organization_id;
|
||||
|
||||
// Read and create the collections
|
||||
let collections: Vec<_> = data
|
||||
|
@@ -1,9 +1,10 @@
|
||||
use std::{io::Read, path::Path};
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use multipart::server::{save::SavedData, Multipart, SaveResult};
|
||||
use rocket::{http::ContentType, response::NamedFile, Data};
|
||||
use rocket_contrib::json::Json;
|
||||
use rocket::form::Form;
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::fs::TempFile;
|
||||
use rocket::serde::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
@@ -31,9 +32,9 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||
]
|
||||
}
|
||||
|
||||
pub fn purge_sends(pool: DbPool) {
|
||||
pub async fn purge_sends(pool: DbPool) {
|
||||
debug!("Purging sends");
|
||||
if let Ok(conn) = pool.get() {
|
||||
if let Ok(conn) = pool.get().await {
|
||||
Send::purge(&conn);
|
||||
} else {
|
||||
error!("Failed to get DB connection while purging sends")
|
||||
@@ -177,25 +178,23 @@ fn post_send(data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Not
|
||||
Ok(Json(send.to_json()))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct UploadData<'f> {
|
||||
model: Json<crate::util::UpCase<SendData>>,
|
||||
data: TempFile<'f>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
enforce_disable_send_policy(&headers, &conn)?;
|
||||
|
||||
let boundary = content_type.params().next().expect("No boundary provided").1;
|
||||
let UploadData {
|
||||
model,
|
||||
mut data,
|
||||
} = data.into_inner();
|
||||
let model = model.into_inner().data;
|
||||
|
||||
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)?;
|
||||
enforce_disable_hide_email_policy(&data.data, &headers, &conn)?;
|
||||
enforce_disable_hide_email_policy(&model, &headers, &conn)?;
|
||||
|
||||
// Get the file length and add an extra 5% to avoid issues
|
||||
const SIZE_525_MB: u64 = 550_502_400;
|
||||
@@ -212,45 +211,27 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn
|
||||
None => SIZE_525_MB,
|
||||
};
|
||||
|
||||
// Create the Send
|
||||
let mut send = create_send(data.data, headers.user.uuid)?;
|
||||
let file_id = crate::crypto::generate_send_id();
|
||||
|
||||
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_path = Path::new(&CONFIG.sends_folder()).join(&send.uuid).join(&file_id);
|
||||
let size = data.len();
|
||||
if size > size_limit {
|
||||
err!("Attachment storage limit exceeded with this file");
|
||||
}
|
||||
|
||||
// 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 file_id = crate::crypto::generate_send_id();
|
||||
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(&send.uuid);
|
||||
let file_path = folder_path.join(&file_id);
|
||||
tokio::fs::create_dir_all(&folder_path).await?;
|
||||
data.persist_to(&file_path).await?;
|
||||
|
||||
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 storage 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)));
|
||||
o.insert(String::from("SizeName"), Value::String(crate::util::get_display_size(size as i32)));
|
||||
}
|
||||
send.data = serde_json::to_string(&data_value)?;
|
||||
|
||||
@@ -367,10 +348,10 @@ fn post_access_file(
|
||||
}
|
||||
|
||||
#[get("/sends/<send_id>/<file_id>?<t>")]
|
||||
fn download_send(send_id: SafeString, file_id: SafeString, t: String) -> Option<NamedFile> {
|
||||
async fn download_send(send_id: SafeString, file_id: SafeString, t: String) -> Option<NamedFile> {
|
||||
if let Ok(claims) = crate::auth::decode_send(&t) {
|
||||
if claims.sub == format!("{}/{}", send_id, file_id) {
|
||||
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).ok();
|
||||
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();
|
||||
}
|
||||
}
|
||||
None
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use data_encoding::BASE32;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use chrono::Utc;
|
||||
use data_encoding::BASE64;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
|
||||
use crate::{
|
||||
api::{core::two_factor::_generate_recover_code, ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData},
|
||||
@@ -152,7 +152,7 @@ fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
|
||||
}
|
||||
|
||||
#[post("/two-factor/duo", data = "<data>")]
|
||||
fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: EnableDuoData = data.into_inner().data;
|
||||
let mut user = headers.user;
|
||||
|
||||
@@ -163,7 +163,7 @@ fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn)
|
||||
let (data, data_str) = if check_duo_fields_custom(&data) {
|
||||
let data_req: DuoData = data.into();
|
||||
let data_str = serde_json::to_string(&data_req)?;
|
||||
duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
|
||||
duo_api_request("GET", "/auth/v2/check", "", &data_req).await.map_res("Failed to validate Duo credentials")?;
|
||||
(data_req.obscure(), data_str)
|
||||
} else {
|
||||
(DuoData::secret(), String::new())
|
||||
@@ -185,11 +185,11 @@ fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn)
|
||||
}
|
||||
|
||||
#[put("/two-factor/duo", data = "<data>")]
|
||||
fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
activate_duo(data, headers, conn)
|
||||
async fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
activate_duo(data, headers, conn).await
|
||||
}
|
||||
|
||||
fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
||||
async fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
||||
use reqwest::{header, Method};
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -209,7 +209,8 @@ fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> Em
|
||||
.basic_auth(username, Some(password))
|
||||
.header(header::USER_AGENT, "vaultwarden:Duo/1.0 (Rust)")
|
||||
.header(header::DATE, date)
|
||||
.send()?
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
|
||||
use crate::{
|
||||
api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData},
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use data_encoding::BASE32;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
@@ -158,14 +158,14 @@ fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Header
|
||||
disable_twofactor(data, headers, conn)
|
||||
}
|
||||
|
||||
pub fn send_incomplete_2fa_notifications(pool: DbPool) {
|
||||
pub async fn send_incomplete_2fa_notifications(pool: DbPool) {
|
||||
debug!("Sending notifications for incomplete 2FA logins");
|
||||
|
||||
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let conn = match pool.get() {
|
||||
let conn = match pool.get().await {
|
||||
Ok(conn) => conn,
|
||||
_ => {
|
||||
error!("Failed to get DB connection in send_incomplete_2fa_notifications()");
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
use u2f::{
|
||||
messages::{RegisterResponse, SignResponse, U2fSignRequest},
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
use url::Url;
|
||||
use webauthn_rs::{base64_data::Base64UrlSafeData, proto::*, AuthenticationState, RegistrationState, Webauthn};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::Route;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
use yubico::{config::Config, verify};
|
||||
|
||||
|
Reference in New Issue
Block a user